rom-rb / rom

Data mapping and persistence toolkit for Ruby
https://rom-rb.org
MIT License
2.08k stars 161 forks source link

Configure how nullable values are treated by the struct compiler #519

Closed waiting-for-dev closed 5 years ago

waiting-for-dev commented 5 years ago

Be able to change the way that the struct compiler treats nullable values from the datastore. Right now, they end up being whether nil or an actual value. It would be nice to have it being configurable, for example, to translate them into the Dry::Monads::Maybe type.

For that, I would:

Examples

class Users < ROM::Relation[:sql]
  auto_struct true
  struct_compiler My::Struct::Compiler

  schema do
    attribute :id, Types::Integer
    attribute :name, Types::String
    attribute :age, Types::Integer.optional

    associations do
      belongs_to :team
    end
  end
end

# ...

user = user_relation.where(id: 1).one.age # => Some(18)
user = user_relation.where(id: 2).one.age # => None
user = user_relation.combine(:team).where(id: 2).one.team # => None

Resources

solnic commented 5 years ago

I reported #528 which covers the part of configuring a custom struct compiler class. Could you update the description here and provide an example of the API where you define how nullable values should be treated?

waiting-for-dev commented 5 years ago

I reported #528 which covers the part of configuring a custom struct compiler class.

@solnic thanks, #528 looks great!

Could you update the description here and provide an example of the API where you define how nullable values should be treated?

Actually, I'm not 100% sure about how we should tackle it. What I want is to be able to process how optional types should be treated, no mind if they are an optional attribute or a belongs_to or has_one relation. If this needs to be done in rom's struct compiler, maybe something like this would be fine:

class MyStructCompiler < StructCompiler
  do_with_optional -> (type) { type.maybe }
end

But I wonder if this could be done at dry-struct or even dry-types level...

solnic commented 5 years ago

@waiting-for-dev I believe this is something that could be done via transform_types in Dry::Struct. See dry-struct recipes.

waiting-for-dev commented 5 years ago

Thanks for your tip @solnic . However, I think it doesn't fulfill the feature:

require 'dry/types'
require 'dry/struct'
require 'dry/monads/maybe'

class Foo < Dry::Struct
  transform_types do |type|
    if type.optional?
      type.constructor do |value|
        Dry::Monads::Maybe.coerce(value)
      end
    else
      type
    end
  end

  attribute :foo1, Dry::Types['strict.string'].optional
end

Foo.new(foo1: nil)
# => [Foo.new] nil (NilClass) has invalid type for :foo1 violates constraints (type?(String, None) failed) (Dry::Struct::Error)

The type remains being a Dry::Types['strict.string'].optional, so it crashes when the value is transformed to Dry::Monads::Maybe::None (and same for Dry::Monads::Maybe::Some).

I first tried transforming the type itself, which seems the appropriate thing to do, but then I always get nil not sure why:

class Foo < Dry::Struct
  transform_types do |type|
    type.optional? ? type.right.maybe : type
  end

  attribute :foo1, Dry::Types['strict.string'].optional
end

puts Foo.new(foo1: nil).foo1.inspect
# => nil
puts Foo.new(foo1: "a").foo1.inspect
# => nil
flash-gordon commented 5 years ago

This is fixed by https://github.com/dry-rb/dry-types/pull/329

waiting-for-dev commented 5 years ago

Thanks @flash-gordon for your quick action.

This is super neat! I don't need to configure anything in the struct compiler. I just have to create a parent struct with the transform_types stuff and inherit all my entities from it. @solnic, I guess #528 can still be useful for other scenarios, but this issue can be closed.

Thanks for all your help.