interactify 0.3.0.pre.alpha.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +23 -0
- data/CHANGELOG.md +17 -0
- data/README.md +33 -44
- data/gemfiles/.bundle/config +2 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile +18 -0
- data/gemfiles/no_railties_no_sidekiq.gemfile.lock +127 -0
- data/gemfiles/railties_6.gemfile +14 -0
- data/gemfiles/railties_6.gemfile.lock +253 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile +19 -0
- data/gemfiles/railties_6_no_sidekiq.gemfile.lock +159 -0
- data/gemfiles/railties_6_sidekiq.gemfile +20 -0
- data/gemfiles/railties_6_sidekiq.gemfile.lock +168 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile +19 -0
- data/gemfiles/railties_7_no_sidekiq.gemfile.lock +158 -0
- data/gemfiles/railties_7_sidekiq.gemfile +20 -0
- data/gemfiles/railties_7_sidekiq.gemfile.lock +167 -0
- data/lib/interactify/async/job_klass.rb +63 -0
- data/lib/interactify/async/job_maker.rb +58 -0
- data/lib/interactify/async/jobable.rb +96 -0
- data/lib/interactify/async/null_job.rb +23 -0
- data/lib/interactify/configuration.rb +15 -0
- data/lib/interactify/contracts/call_wrapper.rb +19 -0
- data/lib/interactify/contracts/failure.rb +8 -0
- data/lib/interactify/contracts/helpers.rb +81 -0
- data/lib/interactify/contracts/mismatching_promise_error.rb +19 -0
- data/lib/interactify/contracts/promising.rb +36 -0
- data/lib/interactify/contracts/setup.rb +39 -0
- data/lib/interactify/dsl/each_chain.rb +96 -0
- data/lib/interactify/dsl/if_interactor.rb +81 -0
- data/lib/interactify/dsl/if_klass.rb +82 -0
- data/lib/interactify/dsl/organizer.rb +32 -0
- data/lib/interactify/dsl/unique_klass_name.rb +23 -0
- data/lib/interactify/dsl/wrapper.rb +74 -0
- data/lib/interactify/dsl.rb +12 -6
- data/lib/interactify/rspec_matchers/matchers.rb +68 -0
- data/lib/interactify/version.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/callable_representation.rb +2 -2
- data/lib/interactify/{interactor_wiring → wiring}/constants.rb +4 -4
- data/lib/interactify/{interactor_wiring → wiring}/error_context.rb +1 -1
- data/lib/interactify/{interactor_wiring → wiring}/files.rb +1 -1
- data/lib/interactify/{interactor_wiring.rb → wiring.rb} +5 -5
- data/lib/interactify.rb +58 -38
- metadata +49 -72
- data/lib/interactify/async_job_klass.rb +0 -61
- data/lib/interactify/call_wrapper.rb +0 -17
- data/lib/interactify/contract_failure.rb +0 -6
- data/lib/interactify/contract_helpers.rb +0 -71
- data/lib/interactify/each_chain.rb +0 -88
- data/lib/interactify/if_interactor.rb +0 -70
- data/lib/interactify/interactor_wrapper.rb +0 -72
- data/lib/interactify/job_maker.rb +0 -56
- data/lib/interactify/jobable.rb +0 -92
- data/lib/interactify/mismatching_promise_error.rb +0 -17
- data/lib/interactify/organizer.rb +0 -30
- data/lib/interactify/promising.rb +0 -34
- data/lib/interactify/rspec/matchers.rb +0 -67
- data/lib/interactify/unique_klass_name.rb +0 -21
| @@ -0,0 +1,63 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Interactify
         | 
| 4 | 
            +
              module Async
         | 
| 5 | 
            +
                class JobKlass
         | 
| 6 | 
            +
                  attr_reader :container_klass, :klass_suffix
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                  def initialize(container_klass:, klass_suffix:)
         | 
| 9 | 
            +
                    @container_klass = container_klass
         | 
| 10 | 
            +
                    @klass_suffix = klass_suffix
         | 
| 11 | 
            +
                  end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                  def async_job_klass
         | 
| 14 | 
            +
                    klass = Class.new do
         | 
| 15 | 
            +
                      include Interactor
         | 
| 16 | 
            +
                      include Interactor::Contracts
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    attach_call(klass)
         | 
| 20 | 
            +
                    attach_call!(klass)
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                    klass
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                  def attach_call(async_job_klass)
         | 
| 26 | 
            +
                    # e.g. SomeInteractor::AsyncWithSuffix.call(foo: 'bar')
         | 
| 27 | 
            +
                    async_job_klass.send(:define_singleton_method, :call) do |context|
         | 
| 28 | 
            +
                      call!(context)
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def attach_call!(async_job_klass)
         | 
| 33 | 
            +
                    this = self
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    # e.g. SomeInteractor::AsyncWithSuffix.call!(foo: 'bar')
         | 
| 36 | 
            +
                    async_job_klass.send(:define_singleton_method, :call!) do |context|
         | 
| 37 | 
            +
                      # e.g. SomeInteractor::JobWithSuffix
         | 
| 38 | 
            +
                      job_klass = this.container_klass.const_get("Job#{this.klass_suffix}")
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                      # e.g. SomeInteractor::JobWithSuffix.perform_async({foo: 'bar'})
         | 
| 41 | 
            +
                      job_klass.perform_async(this.args(context))
         | 
| 42 | 
            +
                    end
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                  def args(context)
         | 
| 46 | 
            +
                    args = context.to_h.stringify_keys
         | 
| 47 | 
            +
             | 
| 48 | 
            +
                    return args unless container_klass.respond_to?(:expected_keys)
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    restrict_to_optional_or_keys_from_contract(args)
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def restrict_to_optional_or_keys_from_contract(args)
         | 
| 54 | 
            +
                    keys = container_klass.expected_keys.map(&:to_s)
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    optional = Array(container_klass.optional_attrs).map(&:to_s)
         | 
| 57 | 
            +
                    keys += optional
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                    args.slice(*keys)
         | 
| 60 | 
            +
                  end
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/async/job_klass"
         | 
| 4 | 
            +
            require "interactify/async/null_job"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Interactify
         | 
| 7 | 
            +
              module Async
         | 
| 8 | 
            +
                class JobMaker
         | 
| 9 | 
            +
                  attr_reader :opts, :method_name, :container_klass, :klass_suffix
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def initialize(container_klass:, opts:, klass_suffix:, method_name: :call!)
         | 
| 12 | 
            +
                    @container_klass = container_klass
         | 
| 13 | 
            +
                    @opts = opts
         | 
| 14 | 
            +
                    @method_name = method_name
         | 
| 15 | 
            +
                    @klass_suffix = klass_suffix
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  concerning :JobClass do
         | 
| 19 | 
            +
                    def job_klass
         | 
| 20 | 
            +
                      @job_klass ||= define_job_klass
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    private
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def define_job_klass
         | 
| 26 | 
            +
                      return NullJob if Interactify.sidekiq_missing?
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      this = self
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                      invalid_keys = this.opts.symbolize_keys.keys - %i[queue retry dead backtrace pool tags]
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                      raise ArgumentError, "Invalid keys: #{invalid_keys}" if invalid_keys.any?
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                      build_job_klass(opts).tap do |klass|
         | 
| 35 | 
            +
                        klass.const_set(:JOBABLE_OPTS, opts)
         | 
| 36 | 
            +
                        klass.const_set(:JOBABLE_METHOD_NAME, method_name)
         | 
| 37 | 
            +
                      end
         | 
| 38 | 
            +
                    end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                    def build_job_klass(opts)
         | 
| 41 | 
            +
                      Class.new do
         | 
| 42 | 
            +
                        include Sidekiq::Job
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                        sidekiq_options(opts)
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                        def perform(...)
         | 
| 47 | 
            +
                          self.class.module_parent.send(self.class::JOBABLE_METHOD_NAME, ...)
         | 
| 48 | 
            +
                        end
         | 
| 49 | 
            +
                      end
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  def async_job_klass
         | 
| 54 | 
            +
                    JobKlass.new(container_klass:, klass_suffix:).async_job_klass
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/async/job_maker"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Interactify
         | 
| 6 | 
            +
              module Async
         | 
| 7 | 
            +
                module Jobable
         | 
| 8 | 
            +
                  extend ActiveSupport::Concern
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  # e.g. if Klass < Base
         | 
| 11 | 
            +
                  # and Base has a Base::Job class
         | 
| 12 | 
            +
                  #
         | 
| 13 | 
            +
                  # then let's make sure to define Klass::Job separately
         | 
| 14 | 
            +
                  included do |base|
         | 
| 15 | 
            +
                    next if Interactify.sidekiq_missing?
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                    def base.inherited(klass)
         | 
| 18 | 
            +
                      super_klass = klass.superclass
         | 
| 19 | 
            +
                      super_job = super_klass::Job # really spiffing
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      opts = super_job::JOBABLE_OPTS
         | 
| 22 | 
            +
                      jobable_method_name = super_job::JOBABLE_METHOD_NAME
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      to_call = defined?(super_klass::Async) ? :interactor_job : :job_calling
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      klass.send(to_call, opts:, method_name: jobable_method_name)
         | 
| 27 | 
            +
                      super(klass)
         | 
| 28 | 
            +
                    end
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  class_methods do
         | 
| 32 | 
            +
                    # create a Job class and an Async class
         | 
| 33 | 
            +
                    # see job_calling for details on the Job class
         | 
| 34 | 
            +
                    #
         | 
| 35 | 
            +
                    # the Async class is a wrapper around the Job class
         | 
| 36 | 
            +
                    # that allows it to be used in an interactor chain
         | 
| 37 | 
            +
                    #
         | 
| 38 | 
            +
                    # E.g.
         | 
| 39 | 
            +
                    #
         | 
| 40 | 
            +
                    # class ExampleInteractor
         | 
| 41 | 
            +
                    #   include Interactify
         | 
| 42 | 
            +
                    #   expect :foo
         | 
| 43 | 
            +
                    #
         | 
| 44 | 
            +
                    #   include Jobable
         | 
| 45 | 
            +
                    #   interactor_job
         | 
| 46 | 
            +
                    # end
         | 
| 47 | 
            +
                    #
         | 
| 48 | 
            +
                    # doing the following will immediately enqueue a job
         | 
| 49 | 
            +
                    # that calls the interactor ExampleInteractor with (foo: 'bar')
         | 
| 50 | 
            +
                    # ExampleInteractor::Async.call(foo: 'bar')
         | 
| 51 | 
            +
                    #
         | 
| 52 | 
            +
                    # it will also ensure to pluck only the expects from the context
         | 
| 53 | 
            +
                    # so that you can have other non primitive values in the context
         | 
| 54 | 
            +
                    # but the job will only have the expects passed to it
         | 
| 55 | 
            +
                    #
         | 
| 56 | 
            +
                    # obviously you will need to be aware that later interactors
         | 
| 57 | 
            +
                    # in an interactor chain cannot depend on the result of the async
         | 
| 58 | 
            +
                    # interactor
         | 
| 59 | 
            +
                    def interactor_job(method_name: :call!, opts: {}, klass_suffix: "")
         | 
| 60 | 
            +
                      job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
         | 
| 61 | 
            +
                      # with WhateverInteractor::Job you can perform the interactor as a job
         | 
| 62 | 
            +
                      # from sidekiq
         | 
| 63 | 
            +
                      # e.g. WhateverInteractor::Job.perform_async(...)
         | 
| 64 | 
            +
                      const_set("Job#{klass_suffix}", job_maker.job_klass)
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                      # with WhateverInteractor::Async you can call WhateverInteractor::Job
         | 
| 67 | 
            +
                      # in an organizer oro on its oen using normal interactor call call! semantics
         | 
| 68 | 
            +
                      # e.g. WhateverInteractor::Async.call(...)
         | 
| 69 | 
            +
                      #      WhateverInteractor::Async.call!(...)
         | 
| 70 | 
            +
                      const_set("Async#{klass_suffix}", job_maker.async_job_klass)
         | 
| 71 | 
            +
                    end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    # if this was defined in ExampleClass this creates the following class
         | 
| 74 | 
            +
                    # ExampleClass::Job
         | 
| 75 | 
            +
                    # this class ia added as a convenience so you can easily turn a
         | 
| 76 | 
            +
                    # class method into a job
         | 
| 77 | 
            +
                    #
         | 
| 78 | 
            +
                    # Example:
         | 
| 79 | 
            +
                    #
         | 
| 80 | 
            +
                    # class ExampleClass
         | 
| 81 | 
            +
                    #   include Jobable
         | 
| 82 | 
            +
                    #   job_calling method_name: :some_method
         | 
| 83 | 
            +
                    # end
         | 
| 84 | 
            +
                    #
         | 
| 85 | 
            +
                    # # the following class is created that you can use to enqueue a job
         | 
| 86 | 
            +
                    # in the sidekiq yaml file
         | 
| 87 | 
            +
                    # ExampleClass::Job.some_method
         | 
| 88 | 
            +
                    def job_calling(method_name:, opts: {}, klass_suffix: "")
         | 
| 89 | 
            +
                      job_maker = JobMaker.new(container_klass: self, opts:, method_name:, klass_suffix:)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                      const_set("Job#{klass_suffix}", job_maker.job_klass)
         | 
| 92 | 
            +
                    end
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
            end
         | 
| @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Interactify
         | 
| 4 | 
            +
              module Async
         | 
| 5 | 
            +
                class NullJob
         | 
| 6 | 
            +
                  def method_missing(...)
         | 
| 7 | 
            +
                    self
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.method_missing(...)
         | 
| 11 | 
            +
                    self
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def respond_to_missing?(...)
         | 
| 15 | 
            +
                    true
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def self.respond_to_missing?(...)
         | 
| 19 | 
            +
                    true
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Interactify
         | 
| 4 | 
            +
              module Contracts
         | 
| 5 | 
            +
                module CallWrapper
         | 
| 6 | 
            +
                  # https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L114
         | 
| 7 | 
            +
                  # Interactor#run calls Interactor#run!
         | 
| 8 | 
            +
                  # https://github.com/collectiveidea/interactor/blob/57b2af9a5a5afeb2c01059c40b792485cc21b052/lib/interactor.rb#L49
         | 
| 9 | 
            +
                  # Interactor.call calls Interactor.run
         | 
| 10 | 
            +
                  #
         | 
| 11 | 
            +
                  # The non bang methods call the bang methods and rescue
         | 
| 12 | 
            +
                  def run
         | 
| 13 | 
            +
                    @_interactor_called_by_non_bang_method = true
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    super
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/async/jobable"
         | 
| 4 | 
            +
            require "interactify/contracts/call_wrapper"
         | 
| 5 | 
            +
            require "interactify/contracts/failure"
         | 
| 6 | 
            +
            require "interactify/contracts/setup"
         | 
| 7 | 
            +
            require "interactify/dsl/organizer"
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            module Interactify
         | 
| 10 | 
            +
              module Contracts
         | 
| 11 | 
            +
                module Helpers
         | 
| 12 | 
            +
                  extend ActiveSupport::Concern
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  class_methods do
         | 
| 15 | 
            +
                    def expect(*attrs, filled: true)
         | 
| 16 | 
            +
                      Setup.expects(context: self, attrs:, filled:)
         | 
| 17 | 
            +
                    end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                    def promise(*attrs, filled: true, should_delegate: true)
         | 
| 20 | 
            +
                      Setup.promises(context: self, attrs:, filled:, should_delegate:)
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def promising(*args)
         | 
| 24 | 
            +
                      Promising.validate(self, *args)
         | 
| 25 | 
            +
                    end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    def promised_keys
         | 
| 28 | 
            +
                      _interactify_extract_keys(contract.promises)
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def expected_keys
         | 
| 32 | 
            +
                      _interactify_extract_keys(contract.expectations)
         | 
| 33 | 
            +
                    end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                    def optional(*attrs)
         | 
| 36 | 
            +
                      @optional_attrs ||= []
         | 
| 37 | 
            +
                      @optional_attrs += attrs
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      delegate(*attrs, to: :context)
         | 
| 40 | 
            +
                    end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    attr_reader :optional_attrs
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                    private
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                    # this is the most brittle part of the code, relying on
         | 
| 47 | 
            +
                    # interactor-contracts internals
         | 
| 48 | 
            +
                    # so extracted it to here so change is isolated
         | 
| 49 | 
            +
                    def _interactify_extract_keys(clauses)
         | 
| 50 | 
            +
                      clauses.instance_eval { @terms }.json&.rules&.keys
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  included do
         | 
| 55 | 
            +
                    c = Class.new(Contracts::Failure)
         | 
| 56 | 
            +
                    # example self is Whatever::SomeInteractor
         | 
| 57 | 
            +
                    # failure class:  Whatever::SomeInteractor::InteractorContractFailure
         | 
| 58 | 
            +
                    const_set "InteractorContractFailure", c
         | 
| 59 | 
            +
                    prepend Contracts::CallWrapper
         | 
| 60 | 
            +
                    include Dsl::Organizer
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                    on_breach do |breaches|
         | 
| 63 | 
            +
                      breaches = breaches.map { |b| { b.property => b.messages } }.inject(&:merge)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                      Interactify.trigger_contract_breach_hook(context, breaches)
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                      if @_interactor_called_by_non_bang_method == true
         | 
| 68 | 
            +
                        context.fail! contract_failures: breaches
         | 
| 69 | 
            +
                      else
         | 
| 70 | 
            +
                        # e.g. raises
         | 
| 71 | 
            +
                        # SomeNamespace::SomeClass::ContractFailure, {whatever: 'is missing'}
         | 
| 72 | 
            +
                        # but also sending the context into Sentry
         | 
| 73 | 
            +
                        exception = c.new(breaches.to_json)
         | 
| 74 | 
            +
                        Interactify.trigger_before_raise_hook(exception)
         | 
| 75 | 
            +
                        raise exception
         | 
| 76 | 
            +
                      end
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         | 
| @@ -0,0 +1,19 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/contracts/failure"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Interactify
         | 
| 6 | 
            +
              module Contracts
         | 
| 7 | 
            +
                class MismatchingPromiseError < Contracts::Failure
         | 
| 8 | 
            +
                  def initialize(interactor, promising, promised_keys)
         | 
| 9 | 
            +
                    super <<~MESSAGE.chomp
         | 
| 10 | 
            +
                      #{interactor} does not promise:
         | 
| 11 | 
            +
                      #{promising.inspect}
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                      Actual promises are:
         | 
| 14 | 
            +
                      #{promised_keys.inspect}
         | 
| 15 | 
            +
                    MESSAGE
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
            end
         | 
| @@ -0,0 +1,36 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/contracts/mismatching_promise_error"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Interactify
         | 
| 6 | 
            +
              module Contracts
         | 
| 7 | 
            +
                class Promising
         | 
| 8 | 
            +
                  attr_reader :interactor, :promising
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.validate(interactor, *promising)
         | 
| 11 | 
            +
                    new(interactor, *promising).validate
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    interactor
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def initialize(interactor, *promising)
         | 
| 17 | 
            +
                    @interactor = interactor
         | 
| 18 | 
            +
                    @promising = format_keys promising
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def validate
         | 
| 22 | 
            +
                    return if promising == promised_keys
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    raise MismatchingPromiseError.new(interactor, promising, promised_keys)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def promised_keys
         | 
| 28 | 
            +
                    format_keys interactor.promised_keys
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def format_keys(keys)
         | 
| 32 | 
            +
                    Array(keys).compact.map(&:to_sym).sort
         | 
| 33 | 
            +
                  end
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
              end
         | 
| 36 | 
            +
            end
         | 
| @@ -0,0 +1,39 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Interactify
         | 
| 4 | 
            +
              module Contracts
         | 
| 5 | 
            +
                class Setup
         | 
| 6 | 
            +
                  def self.expects(context:, attrs:, filled:)
         | 
| 7 | 
            +
                    new(context:, attrs:, filled:).setup(:expects)
         | 
| 8 | 
            +
                  end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.promises(context:, attrs:, filled:, should_delegate:)
         | 
| 11 | 
            +
                    new(context:, attrs:, filled:, should_delegate:).setup(:promises)
         | 
| 12 | 
            +
                  end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  def initialize(context:, attrs:, filled:, should_delegate: true)
         | 
| 15 | 
            +
                    @context = context
         | 
| 16 | 
            +
                    @attrs = attrs
         | 
| 17 | 
            +
                    @filled = filled
         | 
| 18 | 
            +
                    @should_delegate = should_delegate
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  def setup(meth)
         | 
| 22 | 
            +
                    this = self
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                    @context.send(meth) do
         | 
| 25 | 
            +
                      this.setup_attrs self
         | 
| 26 | 
            +
                    end
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    @context.delegate(*@attrs, to: :context) if @should_delegate
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  def setup_attrs(contract)
         | 
| 32 | 
            +
                    @attrs.each do |attr|
         | 
| 33 | 
            +
                      field = contract.required(attr)
         | 
| 34 | 
            +
                      field.filled if @filled
         | 
| 35 | 
            +
                    end
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
            end
         | 
| @@ -0,0 +1,96 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/dsl/unique_klass_name"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Interactify
         | 
| 6 | 
            +
              module Dsl
         | 
| 7 | 
            +
                class EachChain
         | 
| 8 | 
            +
                  attr_reader :each_loop_klasses, :plural_resource_name, :evaluating_receiver
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def self.attach_klass(evaluating_receiver, plural_resource_name, *each_loop_klasses)
         | 
| 11 | 
            +
                    iteratable = new(each_loop_klasses, plural_resource_name, evaluating_receiver)
         | 
| 12 | 
            +
                    iteratable.attach_klass
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize(each_loop_klasses, plural_resource_name, evaluating_receiver)
         | 
| 16 | 
            +
                    @each_loop_klasses = each_loop_klasses
         | 
| 17 | 
            +
                    @plural_resource_name = plural_resource_name
         | 
| 18 | 
            +
                    @evaluating_receiver = evaluating_receiver
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  # allows us to dynamically create an interactor chain
         | 
| 22 | 
            +
                  # that iterates over the packages and
         | 
| 23 | 
            +
                  # uses the passed in each_loop_klasses
         | 
| 24 | 
            +
                  # rubocop:disable all
         | 
| 25 | 
            +
                  def klass
         | 
| 26 | 
            +
                    this = self
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                    Class.new do                                                                    # class SomeNamespace::EachPackage
         | 
| 29 | 
            +
                      include Interactify                                                           #   include Interactify
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                      expects do                                                                    #   expects do
         | 
| 32 | 
            +
                        required(this.plural_resource_name)                                         #     required(:packages)
         | 
| 33 | 
            +
                      end                                                                           #   end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      define_singleton_method(:source_location) do                                  #   def self.source_location
         | 
| 36 | 
            +
                        const_source_location this.evaluating_receiver.to_s                         #     [file, line]
         | 
| 37 | 
            +
                      end                                                                           #   end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                      define_method(:run!) do                                                       #  def run!
         | 
| 40 | 
            +
                        context.send(this.plural_resource_name).each_with_index do |resource, index|#    context.packages.each_with_index do |package, index|
         | 
| 41 | 
            +
                          context[this.singular_resource_name] = resource                           #       context.package = package
         | 
| 42 | 
            +
                          context[this.singular_resource_index_name] = index                        #       context.package_index = index
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                          self.class.klasses.each do |interactor|                                   #       [A, B, C].each do |interactor|
         | 
| 45 | 
            +
                            interactor.call!(context)                                               #         interactor.call!(context)
         | 
| 46 | 
            +
                          end                                                                       #       end
         | 
| 47 | 
            +
                        end                                                                         #     end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                        context[this.singular_resource_name] = nil                                  #     context.package = nil
         | 
| 50 | 
            +
                        context[this.singular_resource_index_name] = nil                            #     context.package_index = nil
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                        context                                                                     #     context
         | 
| 53 | 
            +
                      end                                                                           #   end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                      define_singleton_method(:klasses) do                                          #   def self.klasses
         | 
| 56 | 
            +
                        klasses = instance_variable_get(:@klasses)                                  #     @klasses ||= Wrapper.wrap_many(self, [A, B, C])
         | 
| 57 | 
            +
                        return klasses if klasses
         | 
| 58 | 
            +
             | 
| 59 | 
            +
                        instance_variable_set(:@klasses, Wrapper.wrap_many(self, this.each_loop_klasses))
         | 
| 60 | 
            +
                      end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                      # "<SomeNamespace::EachPackage iterates_over: [A, B, C]>"
         | 
| 63 | 
            +
                      define_method(:inspect) do
         | 
| 64 | 
            +
                        "<#{this.namespace}::#{this.iterator_klass_name} iterates_over: #{this.each_loop_klasses.inspect}>"
         | 
| 65 | 
            +
                      end
         | 
| 66 | 
            +
                    end
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                  # rubocop:enable all
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                  def attach_klass
         | 
| 71 | 
            +
                    name = iterator_klass_name
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                    namespace.const_set(name, klass)
         | 
| 74 | 
            +
                    namespace.const_get(name)
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                  def namespace
         | 
| 78 | 
            +
                    evaluating_receiver
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def iterator_klass_name
         | 
| 82 | 
            +
                    prefix = "Each#{singular_resource_name.to_s.camelize}"
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    UniqueKlassName.for(namespace, prefix)
         | 
| 85 | 
            +
                  end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
                  def singular_resource_name
         | 
| 88 | 
            +
                    plural_resource_name.to_s.singularize.to_sym
         | 
| 89 | 
            +
                  end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                  def singular_resource_index_name
         | 
| 92 | 
            +
                    "#{singular_resource_name}_index".to_sym
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
              end
         | 
| 96 | 
            +
            end
         | 
| @@ -0,0 +1,81 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "interactify/dsl/unique_klass_name"
         | 
| 4 | 
            +
            require "interactify/dsl/if_klass"
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            module Interactify
         | 
| 7 | 
            +
              module Dsl
         | 
| 8 | 
            +
                class IfInteractor
         | 
| 9 | 
            +
                  attr_reader :condition, :evaluating_receiver
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def self.attach_klass(evaluating_receiver, condition, succcess_interactor, failure_interactor)
         | 
| 12 | 
            +
                    ifable = new(evaluating_receiver, condition, succcess_interactor, failure_interactor)
         | 
| 13 | 
            +
                    ifable.attach_klass
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  def initialize(evaluating_receiver, condition, succcess_arg, failure_arg)
         | 
| 17 | 
            +
                    @evaluating_receiver = evaluating_receiver
         | 
| 18 | 
            +
                    @condition = condition
         | 
| 19 | 
            +
                    @success_arg = succcess_arg
         | 
| 20 | 
            +
                    @failure_arg = failure_arg
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def success_interactor
         | 
| 24 | 
            +
                    @success_interactor ||= build_chain(@success_arg, true)
         | 
| 25 | 
            +
                  end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                  def failure_interactor
         | 
| 28 | 
            +
                    @failure_interactor ||= build_chain(@failure_arg, false)
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                  # allows us to dynamically create an interactor chain
         | 
| 32 | 
            +
                  # that iterates over the packages and
         | 
| 33 | 
            +
                  # uses the passed in each_loop_klasses
         | 
| 34 | 
            +
                  def klass
         | 
| 35 | 
            +
                    IfKlass.new(self).klass
         | 
| 36 | 
            +
                  end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  # so we have something to attach subclasses to during building
         | 
| 39 | 
            +
                  # of the outer class, before we finalize the outer If class
         | 
| 40 | 
            +
                  def klass_basis
         | 
| 41 | 
            +
                    @klass_basis ||= Class.new do
         | 
| 42 | 
            +
                      include Interactify
         | 
| 43 | 
            +
                    end
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  def attach_klass
         | 
| 47 | 
            +
                    name = if_klass_name
         | 
| 48 | 
            +
                    namespace.const_set(name, klass)
         | 
| 49 | 
            +
                    namespace.const_get(name)
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  def namespace
         | 
| 53 | 
            +
                    evaluating_receiver
         | 
| 54 | 
            +
                  end
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  def if_klass_name
         | 
| 57 | 
            +
                    @if_klass_name ||=
         | 
| 58 | 
            +
                      begin
         | 
| 59 | 
            +
                        prefix = condition.is_a?(Proc) ? "Proc" : condition
         | 
| 60 | 
            +
                        prefix = "If#{prefix.to_s.camelize}"
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                        UniqueKlassName.for(namespace, prefix)
         | 
| 63 | 
            +
                      end
         | 
| 64 | 
            +
                  end
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                  private
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                  def build_chain(arg, truthiness)
         | 
| 69 | 
            +
                    return if arg.nil?
         | 
| 70 | 
            +
             | 
| 71 | 
            +
                    case arg
         | 
| 72 | 
            +
                    when Array
         | 
| 73 | 
            +
                      name = "If#{condition.to_s.camelize}#{truthiness ? 'IsTruthy' : 'IsFalsey'}"
         | 
| 74 | 
            +
                      klass_basis.chain(name, *arg)
         | 
| 75 | 
            +
                    else
         | 
| 76 | 
            +
                      arg
         | 
| 77 | 
            +
                    end
         | 
| 78 | 
            +
                  end
         | 
| 79 | 
            +
                end
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
            end
         |