Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate chains of contracts within organizers #34

Open
michaelherold opened this issue Oct 10, 2019 · 4 comments
Open

Validate chains of contracts within organizers #34

michaelherold opened this issue Oct 10, 2019 · 4 comments
Labels

Comments

@michaelherold
Copy link
Owner

My original vision for this gem was to build a way to validate that the the interactors that you compose with organizers form a valid chain of business logic. When I used interactors, I would end up writing a ton of integration tests to make sure the chain of logic all fit together and to protect against regressions.

I thought it would be cool to do something like this:

class PlaceOrder
  include Interactor::Organizer
  include Interactor::Contracts

  expects do
    required(:card).filled # used in step 2 but not step 1
  end

  promises do
    required(:order).filled
  end

  organize CreateOrder, ChargeCard, SendThankYou
end

# then, in a test support file
Interactor::Contracts.validate_organizers!

The validate_organizers! method would act like FactoryBot's linting capabilities. We could make it part of the test suite or we could make a Rake task out of it. Whatever feels like the best experience.

The idea is that, given the list of interactors passed to organize, we could compare the promises and expectations of the organized interactors plus the contracts of the organizer itself to check whether it's a valid organizer.

I'm happy to mentor anyone willing to take this on. I do not currently use interactors in my work, hence why I haven't implemented this myself. But for people who use the Interactor gem, I think this would be a major benefit.

@PapePathe
Copy link

Interested and willingg to contribute where do we start?

@michaelherold
Copy link
Owner Author

A simple integration test structure to get you started
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe 'Interactor::Contracts::Organizer' do
  let(:create_order) do
    Class.new do
      include Interactor
      include Interactor::Contracts

      expects do
        required(:card).filled
      end

      promises do
        required(:order).filled
      end
    end
  end

  let(:charge_card) do
    Class.new do
      include Interactor
      include Interactor::Contracts

      expects do
        required(:card).filled
        required(:order).filled
      end

      promises do
        required(:order).filled
      end
    end
  end

  let(:send_thank_you) do
    Class.new do
      include Interactor
      include Interactor::Contracts

      expects do
        required(:order).filled
      end

      promises do
        required(:order).filled
      end
    end
  end

  it 'works' do
    create = create_order
    charge = charge_card
    thank = send_thank_you

    organizer = Class.new do
      include Interactor::Organizer
      include Interactor::Contracts

      expects do
        required(:card).filled
      end

      promises do
        required(:order).filled
      end

      organize create, charge, thank
    end

    binding.pry
  end
end

That's a basic frame that gets everything set up how you need it to test. You'll have access to an organizer that responds to .organized that gets a set of interactors. Each of those interactors has a contract that you'll have to figure out how to combine to see if they match the overarching contract on the organizer.

I'm not sure how this will work but it seems like something we should be able to do.

Feel free to add whatever code you feel you need. Ideally, it won't cause any backward-compatibility problems. If it does cause anything else in the test suite to fail we can work through those issues.

Let me know if you have any further questions!

@markburns
Copy link

Oh I've just seen this. I don't know how I missed this issue.

We implemented this internally as a spec that validates all organizers and even nested organizers and outputs a useful error message for failures.

Example failure:

     Failure/Error:
                 expect(interactor).to be_nil, <<~ERROR
                 Organizer:
                   #{organizer.filename}
                   #{organizer.klass}
       
                 Interactor:
                   #{interactor.filename}
                   #{interactor.klass}
       
                 Interactor contains expected keys:
     
       Organizer:
         /home/mark/code/thing/app/interactors/thing.rb
         Thing

       Interactor:
         /home/mark/code/thing/app/interactors/some_other_thing.rb
          SomeOtherThing
     
       Interactor contains expected keys:
       [:some_thnig_with_a_typo]
     
       that are not previously defined in the Organizer or any of its interactors' promises

We've also done some things that help with readability (for us) for defining contracts.

E.g.

include InteractorContracts
expect :foo, filled: true # defaults to true
expect :baz, filled: false

and it removes quite a bit of the boilerplate.

Not sure if you'd want the spec based approach to validation? The spec support file itself has some pretty hairy metaprogramming going on to find all our interactors and even nested interactors of the following kind. It works with heavily namespaced code too.

class SomeOrganizer
  include InteractorOrganizer
  
  class A
    include InteractorContracts
    expect :foo
  end

  class B
    include InteractorContracts
    expect :bar
  end

  organize A, B
end

By the way, we have been using these changes with our production test suite now for about six months.

If either of these things are of interest, I can try and pull together PR(s). I see you set a high bar with your code quality!
I may throw together a draft PR and you can critique at will until it meets your standards.

@markburns
Copy link

markburns commented Dec 29, 2023

@michaelherold

I ended up open sourcing https://github.com/markburns/interactify that depends on interactor-contracts

It's extracted from a large Rails app. It's optionally Rails > 6, ruby > 3.1.4, with sidekiq onlysidekiq optional. at the moment, but I'm open to making it a bit more pluggable if there's an appetite for it .

Also if there's any of this you'd want to pull in/support here upstream, I'd be glad to look into pulling something together.

No worries if not, it's definitely an opinionated approach.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants