One year of Ruby on Rails configuration | Island94.org
I’ve been working professionaly with Ruby on Rails for nearly 15 years (I’m also the author of GoodJob and Spectator Sport). Last year I left GitHub and co-founded a technology startup, Frontdoor Benefits, that helps people enroll and manage their US government welfare benefits like SNAP/EBT.
Therefore, I’ve been working in a fresh Ruby on Rails app full-time now for 1 year. One of the Rails pillars is “convention over configuration”, so I thought it would be fun to share what has so far accumulated in my app’s /config directory: monkeypatches, extensions, and appwide behaviors.
Let’s start with the most controversial one.
Object#not_nil? and boolean extensions
# config/extensions/ext_object_boolean_nil.rb
class Object<br>def not_nil?<br>!nil?<br>end
def false?<br>false<br>end
def true?<br>false<br>end<br>end
class FalseClass<br>def false?<br>true<br>end<br>end
class TrueClass<br>def true?<br>true<br>end<br>end
I realize there are PhDs written about the evils of null. I DISAGREE. I am continually confronted with the need to distnguish between “Yes”, “No”, and “has not answered the question yet” in my my wide models. I want a simple predicate pair like present?/blank? for nil , hence nil? /not_nil? .
And then you can see where that led me wanting to distinguish between truthy and falsey and true true and false false. Predicates are great!
timestamptz default
# config/initializers/active_record_timezones.rb
ActiveSupport.on_load(:active_record_postgresqladapter) do<br>ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz<br>end
I write my database migrations using table.datetime and it generates timestamptz columns. I guess this is fine.
Capybara.threadsafe
# config/initializers/capybara.rb
Capybara.threadsafe = true
I use Capybara as my harness for webdriving automations, rather than writing raw Selenium (never again!).
Custom Types
# config/initializers/custom_types.rb
Rails.application.config.to_prepare do<br>ActiveRecord::Type.register(:phone_number, PhoneNumber::Attribute)<br>ActiveModel::Type.register(:phone_number, PhoneNumber::Attribute)<br>end
module Kernel<br>def PhoneNumber(value) # rubocop:disable Naming/MethodName<br>value.is_a?(PhoneNumber) ? value : PhoneNumber.new(value)<br>end<br>end
There are several annoyances here:
I have to register custom attributes twice: once for Active Record and once for Active Model.
I want my types to live in /app, and be autoloaded, but there isn’t a lifecycle hook for that
And one celebration: I love Ruby and being able to coerce withPhoneNumber(something)
dom_target
# config/initializers/extend_action_view_record_identifier.rb
module ActionView<br>module RecordIdentifier<br>def dom_target(*objects)<br>objects.map do |object|<br>if object.is_a?(Symbol) || object.is_a?(String)<br>object<br>elsif object.is_a?(Class)<br>dom_class(object)<br>else<br>dom_id(object)<br>end<br>end.join(JOIN)<br>end<br>end<br>end
I write a lot of Turbo and I want to be able to chain together an unlimited list of identifiers like (:admin, client, :outbound, Message, :new) to produce something like admin_client_24_outbound_message_new to pair up Turbo Streams and Broadcasts. I upstreamed this into Rails 8.1 so this is unecessary now🎉
field_error_proc
# config/initializers/form_errors.rb
ActiveSupport.on_load(:action_view) do<br>ActionView::Base.field_error_proc = proc do |html_tag, _instance_tag|<br>html_tag<br>end<br>end
Still necessary 🫥
GoodJob and UUIDv7
# config/initializers/good_job.rb
Rails.application.configure do<br>config.good_job.execution_mode = :inline if Rails.env.test?<br>end
GoodJob.preserve_job_records = true<br>GoodJob.on_thread_error = ->(exception) { Appsignal.send_error(exception) }
ActiveSupport.on_load(:action_mailer) do<br>ActionMailer::MailDeliveryJob.retry_on StandardError, wait: :polynomially_longer, attempts: Float::INFINITY<br>ActionMailer::MailDeliveryJob.discard_on ActiveJob::DeserializationError<br>end
# **SNIP** Lots of GoodJob cron config
module ActiveJobUUIDv7<br>def initialize(*args)<br>super<br>@job_id = SecureRandom.uuid_v7<br>end<br>ruby2_keywords(:initialize)<br>end
ActiveSupport.on_load(:active_job) do<br>include ActiveJobUUIDv7<br>end
ActiveSupport.on_load(:good_job_execution) do<br>before_create { self.id ||= SecureRandom.uuid_v7 }<br>end
ActiveSupport.on_load(:good_job_process) do<br>before_create { self.id ||= SecureRandom.uuid_v7 }<br>end
ActiveSupport.on_load(:good_job_batch_record) do<br>before_create { self.id ||= SecureRandom.uuid_v7 }<br>end
ActiveSupport.on_load(:good_job_setting) do<br>before_create { self.id ||= SecureRandom.uuid_v7 }<br>end
A surprising misconception I’ve run across with folks using GoodJob is that using more configuration is better. Less less less.
Specifically at the bottom, I’ve patched GoodJob and Active Job to use UUIDv7 to see if it’s any better. The verdict is still out. It’s not worse!
I18n verification
# config/initializers/i18n_verify
return unless Rails.configuration.i18n.raise_on_missing_translations
IGNORED_I18N_KEYS =...