Some generic top level type configuration?


#1

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.

What do you think?


#2

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.

Another option is to use configuration events to hook into schema setup and adjust schemas for all relations. ie here you can see how this is used in rom-sql.

Try to experiment with this and let me know if you have any questions.


#3

Thanks for your feedback @solnic

I’ll try to experiment with this and report anything I think could be useful.


#4

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!! :smile:) :

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.