Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rails model enum types are too generic #1700

Closed
fazo96 opened this issue Oct 27, 2023 · 4 comments
Closed

Rails model enum types are too generic #1700

fazo96 opened this issue Oct 27, 2023 · 4 comments

Comments

@fazo96
Copy link

fazo96 commented Oct 27, 2023

While adopting tapioca on our project I noticed two issues with rails model enums:

  1. Looks like our enums are typed as T.nilable(::String) even though the actual possible values for the enum should be available by tapioca to generate
  2. Even if the enums were to be typed correctly, there is no exposure of the type alias to be able to use it in sorbet type signatures in the application code (not even sure if this can be done and to what extent, I am new to sorbet and tapioca)

The code generating the types for the enums seems to be this:

sig { params(enum_map: T::Hash[T.untyped, T.untyped]).returns(String) }
def type_for_enum(enum_map)
value_type = enum_map.values.map { |v| v.class.name }.uniq
value_type = if value_type.length == 1
value_type.first
else
"T.any(#{value_type.join(", ")})"
end
"T::Hash[T.any(String, Symbol), #{value_type}]"
end

I wonder if it's possible to solve both problems. The main method to get the enum value seems to be generated by some other Compiler though so it might be tricky

I might give it a shot to implement this myself, recommendations and suggestions are welcome

@KaanOzkan
Copy link
Contributor

I'm not sure I understand the problems, for the first one can you post the enum definition and the generated DSL RBI?

For #2 I'm not sure what you mean by type alias. Do you have a concrete example usage you can share?

@fazo96
Copy link
Author

fazo96 commented Oct 29, 2023

hi @KaanOzkan, let me try making a concrete example of what I mean:

Problem 1:

If I have this rails model:

class Person < ApplicationRecord
  enum favorite_color: { blue: 'blue', red: 'red' }
end

This means accessing person.favorite_color can have a value that is either :blue or :red, so I expect the following type definitions in person.rbi:

sig { returns(T.nilable(T.any(:red, :blue))) }
def favorite_color; end

Instead I get:

sig { returns(T.nilable(String)) }
def favorite_color; end

Poking around the class at runtime it is possible to pull the list of possible values for an enum so this might be solveable

Problem 2:

Assuming problem 1 is fixed and the signature is what I expect, I would like to be able to reference the T.any(:red, :blue) type. Imagine it is assigned to a constant called Person::FavoriteColorEnum I should be able to write the following type signature

sig { params(favorite_color: Person::FavoriteColorEnum) }
def do_stuff_with_favorite_color!(favorite_color)
  ...
end

This way if changes are made to the actual enum definition, all type signatures referencing it are also updated (after running tapioca dsl)

I imagine it might look something like this in person.rbi:

class Person < ApplicationRecord
  # I don't know if this is the right way to do it, just how I imagine it coming from a Typescript background
  FavoriteColorEnum = T.any(:red, :blue)

  ...

  sig { returns(T.nilable(Person::FavoriteColorEnum)) }
  def favorite_color; end
end

I am not sure how that constant would be available at runtime. I guess it would not: similarly to Person::PrivateRelation as explained in #1266

If it helps, I am coming from Typescript and only this week I started using Sorbet, so the ecosystem here is all new to me.

Hopefully it's clear now 🙂

@KaanOzkan
Copy link
Contributor

The individual symbols red and blue are not types so you can't use them in a type signature: sorbet.run.

If you want to use enum values in your signatures you need to use T::Enum (https://sorbet.org/docs/tenum & https://sorbet.org/docs/tenum#enum-values-in-types) which is separate from ActiveRecord Enums.

@fazo96
Copy link
Author

fazo96 commented Nov 10, 2023

I see how this can't be done now. In sorbet, T.any(:red, :blue) is not a valid type, to get something similar T::Enum should be used.

However T::Enum is not just a type, it's an actual class, so there is no way to generate a T::Enum definition from the active record enums, because the T::Enum would be a different thing at runtime and runtime code would need to be written to convert from T::Enum to symbols suitable for active record enums.

The root problem preventing this use case is that it's not possible to define a type as an enum of values T.any(:red, :blue) but only as an enum of types, but that's a Sorbet problem and not a tapioca problem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants