I know you can define the type for each attribute in a one-by-one basis. However, it would be nice to be able to configure something in a layer above all of them. I’m being quite abstract because I’m not sure which would be the best way to accomplish it. What I have in mind is the idea to configure how nil values are managed. For example, I’d like to configure every optional attribute in my app to be returned as the Some/Nothing in dry-monads gem.
This type of functionality could be provided by the schema inferers. Right now there’s no way of configuring them. You could try experimenting with a customized TypeBuilder class which inferrers use. Here’s how its default implementation looks like.
You could subclass it and adjust its logic however you like, and register it for your database type:
class MyTypeBuilder < ROM::SQL::Schema::TypeBuilder
# go nuts
end
ROM::SQL::Schema::TypeBuilder.register(:postgres, MyTypeBuilder)
Unfortunately, it wasn’t designed with such adjustments in mind, so you will find yourself duplicating code and changing it. ie here we handle columns that allow null and there’s no way of adjusting just that bit. If we wanted to expose this kind of configuration, it would have to be provided through a nice public API, rather than subclassing and adding new methods.
I revisited this issue and I found an alternative solution which plays very well with rom nature of multiple small layers one on top of the other.
I would leave schema inferrers as they are. Instead, we can use struct compiler to deal with nil values in one or other way. Furthermore, it has one extra beneffit: struct compiler can deal not only with nullable attributes but also with nullable associations. Schema inferrers could be changed to return a Maybe when a value is nil, but they can do nothing with nullable associations.
I have done a POC in my project and I’m very happy to see how easy it has been (really nice decouplings in rom!! ) :
require "dry/types/extensions/maybe"
require "rom/struct_compiler"
ROM::StructCompiler.class_eval do
def visit_relation(node)
_, header, meta = node
name = meta[:combine_name] || meta[:alias]
namespace = meta.fetch(:struct_namespace)
model = meta[:model] || call(name, header, namespace)
member =
if model < Dry::Struct
model
else
Dry::Types::Definition.new(model).constructor(&model.method(:new))
end
if meta[:combine_type] == :many
[name, Types::Array.of(member)]
else
[name, member.maybe]
end
end
# @api private
def visit_sum(node)
*types, meta = node
type = types.map { |type| visit(type) }.reduce(:|).meta(meta)
return type unless type.left.primitive == NilClass
type.right.maybe
end
end
For attributes, it is as easy as to extend with the visit_sum method. For associations, I had to rewrite #call method, but, in fact, the only modified line is from:
[name, member.optional]
to:
[name, member.maybe]
I think that a good improvement for rom would be:
Be able to register the struct compiler to use. By default, use the current one.
Modify current struct compiler so that it is easier to extend in order to modify how nil values are treated, or, even better, make it accept two lambdas as options doing the actual work for nill attributes and relations, respectively. As before, default to the current behaviour.
Consider shipping with a small extension using dry-monads's Maybe for that purpose.
Maybe some of this could be send upstream to dry-types.
What do you think? If you like the idea, I could prepare a PR with a bit of time.
@waiting-for-dev this recently came up in some other discussion. I think it’s a really good idea to make StructCompiler configurable. I reported an issue about it with a description of how I envision this could be done. Let’s take it from there
We ran into this issue early on as well, but decided to explicitly write all attributes in the relation schema. More boilerplate, but improves the dev experience because it works as living documentation.