Defining nested Ruby constants
Basics
Just so that we have something to build on, let’s start with normal constants in Ruby.
They are usually defined in upper case with underscores.
class Account
STATUS_ACTIVE = 'active'
STATUS_BANNED = 'banned'
end
Problems with getting all constants
The most common way of having all of them available, is to just define it, like this:
class Account
STATUS_ACTIVE = 'active'
STATUS_BANNED = 'banned'
ALL_STATUSES = [STATUS_ACTIVE, STATUS_BANNED]
end
In th past, I needed it for:
- generating API documentation and showing all statuses
Account::ALL_STATUSES.to_sentence => "active and banned"
- generating
<select>
HTML element - validation
validates :status, inclusion: { in: ALL_STATUSES }
(more in docs)
The main downside of this approach is that you have to remember to include it in ALL_STATUSES
. For simpler cases
is not a problem, but it gets hairy for more complicated cases, such as:
class Request
SUCCESS = 200
CREATED = 201
CLIENT_ERROR = 400
NOT_FOUND = 404
SERVER_ERROR = 500
SUCCESS_CODES = [SUCCESS, CREATED]
CLIENT_ERROR_CODES = [CLIENT_ERROR, NOT_FOUND]
SERVER_ERRORS = [SERVER_ERROR]
# More common approach
ALL_CODES = [SUCCESS, CREATED, CLIENT_ERROR, NOT_FOUND, SERVER_ERROR]
# Little bit smarter approach
ALL_CODES = SUCCESS_CODES + CLIENT_ERROR_CODES + SERVER_ERRORS
end
Now when you add a new code, you’ll need to update the right success/client errors/server errors hash and potentially
even ALL_CODES
.
DISCLAIMER: Please do not roll you own HTTP client, this is just an example. In my previous jobs we dealt with payment statuses, loan application statuses, fraud statuses where it isn’t that obvious how they relate to each other. Hence, we were more likely to make and a mistake, and unfortunately we made it a few times.
The trick
The core of the “trick” is to understand return value of the constant definition is just the value you assigned:
2.7.2 : 001 > CONSTANT = 1
=> 1
This allows us to write the Request
like this:
class Request
ALL_CODES = [
SUCCESS_CODES = [
SUCCESS = 200,
CREATED = 201,
],
CLIENT_ERROR_CODES = [
CLIENT_ERROR = 400,
NOT_FOUND = 404,
],
SERVER_ERRORS = [
SERVER_ERROR = 500
],
].flatten
end
Nicer git diff
When introducing new constant, with the smarter approach, the git diff is super obvious:
class Request
ALL_CODES = [
SUCCESS_CODES = [
SUCCESS = 200,
CREATED = 201,
],
CLIENT_ERROR_CODES = [
CLIENT_ERROR = 400,
+ FORBIDDEN = 403,
NOT_FOUND = 404,
],
SERVER_ERRORS = [
SERVER_ERROR = 500
],
].flatten
end
Compared to “flattened” approach you have to update multiple places and as a reviewer, it is a bit trickier to know what’s changing
class Request
SUCCESS = 200
CREATED = 201
CLIENT_ERROR = 400
+ FORBIDDEN = 403
NOT_FOUND = 404
SERVER_ERROR = 500
SUCCESS_CODES = [SUCCESS, CREATED]
- CLIENT_ERROR_CODES = [CLIENT_ERROR, NOT_FOUND]
+ CLIENT_ERROR_CODES = [CLIENT_ERROR, FORBIDDEN, NOT_FOUND]
SERVER_ERRORS = [SERVER_ERROR]
# More common approach
- ALL_CODES = [SUCCESS, CREATED, CLIENT_ERROR, NOT_FOUND, SERVER_ERROR]
+ ALL_CODES = [SUCCESS, CREATED, CLIENT_ERROR, FORBIDDEN, NOT_FOUND, SERVER_ERROR]
# Little bit smarter approach
ALL_CODES = SUCCESS_CODES + CLIENT_ERROR_CODES + SERVER_ERRORS
end
Conclusion
This obvious, yet simple trick shows the structure via nesting, is less error prone, git diff
is clearer, and uses just
plain Ruby. What’s not to love?
Got questions, comments, or feedback? Reach out at:
Did you know you can define #Ruby constants in a nested way?
— Miroslav Csonka (@miroslavcsonka) April 1, 2021
Visit https://t.co/RV0yKUDhlG for a deeper explanation with examples pic.twitter.com/4mlv3KkkSr