My app actually needs to handle money amounts. Following @alassekadvice, 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.
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
# 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.
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.