contracts.ruby icon indicating copy to clipboard operation
contracts.ruby copied to clipboard

Contracts and order of includes in the app

Open timuckun opened this issue 6 years ago • 4 comments

I have a contract like this

 Contract Maybe[String], Maybe[String] => User
  def self.create_user
    validate_password(password, password_confirmation)
    hashed_password = DB["select crypt( ? , gen_salt('bf'));", password].first[:crypt]
    u = User.new(email: email)
    DB.transaction do
      u.save
      u.add_password_hash(hashed_password: hashed_password)
    end
  end

When the app includes the module this code is in I get an error message that says

uninitialized constant Svc::User (NameError)

So I tried ::User instead but that doesn't either. So it looks like I need to load all other classes first before I can use contracts that use them.

Is there a way to get around this?

timuckun avatar Sep 24 '17 10:09 timuckun

Does it work without the contract? I would expect the behavior of uninitialized constant to be consistent here since the code doesn't get executed until the method is called.

egonSchiele avatar Sep 24 '17 15:09 egonSchiele

Whoops, accidentally closed.

egonSchiele avatar Sep 24 '17 15:09 egonSchiele

Yes it does work if I take the contract out.

I guess it makes sense that the contract can't know about a type that hasn't been defined yet but this means I have to be very careful about where I include my contracts. It's a bit annoying for custom classes. Either that or I only return simple types which doesn't seem all that satisfactory either.

timuckun avatar Sep 25 '17 23:09 timuckun

I ran into this as well. I worked around it by stubbing any classes that are referenced in contracts before you actually load those classes. Then you don't have to worry about load order.

Example:

# ---------------
# lib/mygem.rb
# ---------------
module MyGem
  class ClassOne; end
  class ClassTwo; end
end

require "contracts"
require_relative "mygem/classone"
require_relative "mygem/classtwo"

module MyGem
  def self.some_method
    # Other stuff can go here if you need it.
  end
end

# ---------------
#  lib/mygem/classone.rb
# ---------------
module MyGem
  class ClassOne
    include ::Contracts::Core

    Contract MyGem::ClassTwo => String
    def something_that_calls_classtwo(classtwo_obj)
      # Do something with classtwo_obj
    end
  end
end

# ---------------
#  lib/mygem/classtwo.rb
# ---------------
module MyGem
  class ClassTwo
    include ::Contracts::Core
 
    Contract MyGem::ClassOne => String
    def something_that_calls_classone(classone_obj)
      # Do something with classone_obj
    end
  end
end

kpaulisse avatar Feb 18 '18 20:02 kpaulisse