Skip to content

Instantly share code, notes, and snippets.

@profh
Last active November 6, 2023 02:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save profh/752f69f117c6ab4bf543e7001026ae6e to your computer and use it in GitHub Desktop.
Save profh/752f69f117c6ab4bf543e7001026ae6e to your computer and use it in GitHub Desktop.

Notes on Enums in Rails

Enums in Rails are relatively new, but also incredibly useful. When we create an enum, we get lots of other things that come along with that for free. Consider this example from PATS Medicine model:

enum :admin_method, { oral: 1, injection: 2, intravenous: 3, topical: 4}, scopes: true

When I create this, because I have specified scopes: true, I automatically get scopes written for Medicine.oral, Medicine.injection, Medicine.intravenous, and Medicine.topical. I still need to test these methods to be sure they were created as intended, but I have confidence that they will work and help us filter medicines by the appropriate admin_method.

Let's say I didn't want to use theirs, but wanted to write my own scopes and pluralize the names (e.g., Medicine.topicals). If I wrote that scope, it would have to be:

scope :topicals, -> { where('admin_method = ?', 4) }

That works, but isn't intuitive or easy to read. You'd have to know the mapping of the enum, which isn't obvious. It would be nicer to write:

scope :topicals, -> { where('admin_method = ?', admin_methods['topical']) }

# or even better...

scope :topicals, -> { where('admin_method = ?', admin_methods[:topical]) }

I can do that because Rails gives me a hash called admin_methods that I can use to access the appropriate values in enum. Using that instead of a hard number does make my code easier to read later.

Now if I am accessing that hash inside the Medicine class (as I have so far), then I simply call it admin_methods[:some-value]. If I reference it outside of the Medicine model (for example, MedicineCost), then I can do so by adding on the model name, as follows: Medicine.admin_costs[:some-value].

If I were to have a scope in a different model that uses this enum, I'd prefer to reference the enum value in this way, because again, it makes the code far easier to understand. Consider which set is easier to understand:

# SET 1

scope :for_admin_method, ->(method) { joins(:medicine).where("admin_method = ?", Medicine.admin_methods[method]) }

assert_equal 3, MedicineCost.for_admin_method(:topical).size

or

# SET 2

scope :for_admin_method, ->(method) { joins(:medicine).where("admin_method = ?", method }

assert_equal 3, MedicineCost.for_admin_method(4).size

Set 2 is easier to write the scope, but harder to apply that scope in other parts of the code, because you will always need to know the appropriate number mapping. In contrast, Set 1 scope is slightly longer because it uses the enum hash, but its application is much easier to use when applying the scope and easier for anyone reading that code to immediately understand. I would strongly encourage you to go the route of Set 1 rather than Set 2.

As I've said in class, we want code that is easy to understand and maintain in the future, because if our system has any real value, it will have to updated by others coming after us for whom things might not be immediately obvious. Writing maintainable code is important and enums give us tools to make that a bit easier.

Another advantage of enums is that we automatically get boolean methods associated with instances. For example:

@carprofen.injection?  # => true
@rabies.oral?          # => false

If in the enum I set suffix: true then these boolean instance methods would then be:

@carprofen.injection_admin_method?  # => true
@rabies.oral_admin_method?          # => false

Finally, there have been some questions about validating enums and I think one thing that was not made clear is that enums are automatically restricted to the set of allowed values in the enum. That is, if you were to try to pass anything along that was not one of these values, then you’d get an Argument Error that it’s not a valid option automatically. Technically, you do not need to write a validation for an enum as it’s automatically covered. Likewise, you don’t have to test it as it's automatically in place. (… unless you overwrite this and turn the option off)

That said, it’s not wrong to test it to be sure it’s there and you didn’t overwrite the automatic code. The shoulda_matchers will work for allowed values, but they won’t work for unallowed values (aside from blank and nil) because an error would occur. That means the only way to test unallowed values aside from blank and nil is via a context, building the employee object with a disallowed run and confirming it is invalid. It is your choice if you want to do this further texting of role within a context.

Qapla'

Prof. H

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment