Trying to add custom type to schema with Types.define(..) raises no method error

Hello there,

My app actually needs to handle money amounts. Following @alassek advice, I store these amounts as integer in my postgres database and want to use Money gem instances everywhere else in my app.

When reading data from relation, I want a coercion to Money instance. Ok, then I copy/paste from Hanami Guides :

# app/relations/credentials.rb
(...)
JWKS = Types.define(JWT::JWK::Set) do
        input { |jwks| Types::PG::JSONB[jwks.export] }
        output { |jsonb| JWT::JWK::Set.new(jsonb.to_h) }
      end

      schema infer: true do
        attribute :jwks, JWKS
      end

and adapt to my use case as :

# app/db/relation.rb
(...)
MoneyType = Types.define(Money) do
  input { |amount| amount.cents }
  output { |int| Money.from_cents(int, "EUR") }
end

# app/relations/entries.rb
(...)
schema :entries, infer: true do
  attribute :amount, MoneyType
end

This results in MyApp::Types:Dry::Types::Module (NoMethodError) complaining about missing define method..

After browsing a few (and long) minutes hours, I came to make this works by changing Types.define(..) to ROM::SQL::Types.define(..).

Does Hanami guides needs an update ? Am I doing these coercions the right way ?
Thanks for any advice here..

This DSL is defined in ROM::SQL::Types module, not in plain dry-types. Types from dry-types know nothing about the “output” part so it cannot be there. ROM::SQL::Types is imported automatically in relations, but from the error you’ve posted, it’s clear that MyApp::Types:Dry::Types::Module is being used. You need to reference the correct types module for it to work.

Thanks for your answer.

It seems like my hanami2.2 app does not import ROM::SQL::Types in relations by default.. but the one that is defined in lib/my_app/types.rb which is Dry::Types.

This is default hanami setting, right ? (I think I didn’t override anything in lib/my_app/types.rb)

You need to show more of your code, how is DB::Relation defined in app/db/relation.rb? Does entries.rb inherit it normally? It should work OK unless you’re messing with normal lexical scope by class_eval/module_eval/Class.new/etc

Here is actual app/db/relation.rb :

# frozen_string_literal: true

require "hanami/db/relation"

module MyApp
  module DB
    class Relation < Hanami::DB::Relation
      auto_struct true

      # My custom coercion types below
      MoneyType = ROM::SQL::Types.define(Money) do
        input { |amount| amount.cents }
        output { |int| Money.from_cents(int, "EUR") }
      end
    end
  end
end

and my actual app/relations/entries.rb :

# frozen_string_literal: true

module MyApp
  module Relations
    class Entries < MyApp::DB::Relation
      schema :entries, infer: true do
        attribute :credit_amount, MoneyType
        attribute :debit_amount, MoneyType

        associations do
          belongs_to :account
          belongs_to :third_party, null: true
        end
      end
    end
  end
end

and, to be complete, lib/my_app/types.rb :

# frozen_string_literal: true

require "dry/types"

module MyApp
  Types = Dry.Types

  module Types
    # Define your custom types here
  end
end

Thanks! Right, it’s not your fault but rather an unfortunate series of events, I’d say. You see, when your relations are defined like this

module MyApp
  module Relations
    # ...

It starts by looking for a Types constant. Since it’s defined within MyApp, it gets chosen.

However, if you write relations like this:

module MyApp::Relations
  # ...

then constant resolution will skip MyApp namespace. It will continue searching in the parent class (in all ancestors to be exact).

Another approach is moving Types out of MyApp:
lib/types.rb:

Types = Dry.Types()

module Types
  #
end

Then even in this scenario:

module MyApp
  module Relations
    class Entries < Hanami::DB::Relation
      #

it will prefer constants from ancestors (hence ROM::SQL::Types. If you ever need your app’s types in a relation you can always reference it with ::Types::Integer.

I wonder how come it wasn’t found earlier. ROM::SQL::Types are nominal by default, that is, they delegate type checks to the database. Dry.Types are strict by default, meaning they will type-check input data, basically just wasting time. This configuration shouldn’t be the default.

Ref Constant resolution in Ruby - valve's

Thanks for you subtle answer.

I prefer keep types.rb inside MyApp module. Then, since Entries inherits from MyApp::DB::Relation, I tried to define Types constant there :

module MyApp
  module DB
    class Relation < Hanami::DB::Relation
      Types = ROM::SQL::Types # attempt to define Types constant in 'Relation' class
     
      # Defining types here works !
      MoneyType = Types.define(Money) do
        (...)
      end
    end
  end
end

But using Types within Entries class like this :

module MyApp
  module Relations
    class Entries < MyApp::DB::Relation
      MoneyType = Types.define(Money) do
        (...)
      end
    end
  end
end

still leads to undefined method 'define' for MyApp::Types:Dry::Types::Module (NoMethodError).

The link you provide explains how Ruby resolves Constants. Not so obvious to me until now. Using Module.nesting.inspect was very enlightening.

Thank you so much.

I think the simplest solution would be:

in you db/relation.rb

module MyApp
  module DB
    class Relation < Hanami::DB::Relation
      Types = superclass::Types

and create my_app/relations.rb:

module MyApp
  module Relations
    Types = ROM::SQL::Types

this should convince ruby rom’s module has priority