chanzuckerberg / sorbet-rails

A set of tools to make the Sorbet typechecker work with Ruby on Rails seamlessly.
MIT License
638 stars 85 forks source link

Default ActiveRecord Serialized type to Coder #449

Closed michaelmcchen closed 3 years ago

michaelmcchen commented 3 years ago

When attempting to serialize a model attribute to a specific type[0], sorbet-rails generates generic type signatures as if that attribute were unknown [1]. That shouldn't be the case, given that we are explicitly serializing & deserializing to a given class.

I am proposing that we pass that directly to the RBI definitions[2]. Given these examples, this will allow us to do

model = Model.first
model.data.a_prop

rather than

model = Model.first
data = T.cast(model.data, Serialized)
data.a_prop

[0] sample model definition

class Model < ActiveRecord::Base
  class Serialized < T::Struct
    sig { params(data: T.nilable(T::Hash[String, String])).returns(T.self_type) }
    def load(data)
      TypeCoerce[self].new.from(data)
    end

    sig { params(data: T.untyped).returns(T.nilable(T::Hash[String, String])) }
    def dump(data)
      data.serialize
    end

    prop :a_prop, T.nilable(String)
  end

  # ie. data's DB column is a Postgres JSONB
  serialize :data, Serialized
end

[1] generated types before this PR

   sig { returns(T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)) }
  def data; end

  sig { params(value: T.any(T::Array[T.untyped], T::Boolean, Float, T::Hash[T.untyped, T.untyped], Integer, String)).void }
  def data=(value); end

  sig { returns(T::Boolean) }
  def data?; end

[2] generated types after this PR

   sig { returns(Model::Serialized) }
  def data; end

  sig { params(value: Model::Serialized).void }
  def data=(value); end

  sig { returns(T::Boolean) }
  def data?; end
hdoan741 commented 3 years ago

Awesome! We've wanted to support serialized attributes and this seems to do exactly that!