egonSchiele / contracts.ruby

Contracts for Ruby.
http://egonschiele.github.com/contracts.ruby
BSD 2-Clause "Simplified" License
1.44k stars 82 forks source link

Contracts and order of includes in the app #268

Open timuckun opened 7 years ago

timuckun commented 7 years ago

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?

egonSchiele commented 7 years ago

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 commented 7 years ago

Whoops, accidentally closed.

timuckun commented 7 years ago

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.

kpaulisse commented 6 years ago

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