Custom application configuration variables in Rails 4 and 5
… and check why 5600+ Rails engineers read also this
Custom application configuration variables in Rails 4 and 5
You probably know that you can configure Rails in config/application.rb
and config/environments/development.rb
etc. But you can also leverage that for configuring your own custom settings for your application.
Single level configuration
# config/environments/development.rb
# or
# config/environments/production.rb
Rails.application.configure do
config.my_custom_setting = "WOW"
end
# config/application.rb
module ApplicationName
class Application < Rails::Application
config.load_defaults 5.1
config.my_custom_setting = "DEFAULT"
end
end
As with normal rails configuration, your own settings in config/environments/*
take precedence over those specified in config/application.rb
.
Nested configuration
If you have multiple settings that you would like to group together, you can prefix them with x.config_name
.
# config/environments/development.rb
# or
# config/environments/production.rb
Rails.application.configure do
config.x.external_api.timeout = 5
end
# config/application.rb
module ApplicationName
class Application < Rails::Application
config.load_defaults 5.1
config.x.external_api.timeout = 30
config.x.external_api.url = "https://example.org/api/path"
end
end
or you can just use Hash
# config/environments/development.rb
# or
# config/environments/production.rb
Rails.application.configure do
config.external_api[:timeout] = 5
end
# config/application.rb
module ApplicationName
class Application < Rails::Application
config.load_defaults 5.1
config.external_api = {
timeout: 30,
url: "https://example.org/api/path"
}
end
end
Accessing in your model or controller
To read those preference, you can use Rails.application.config
or its shorter version Rails.configuration
.
class User < ApplicationRecord
def avatar
avatar_url || Rails.configuration.default_avatar_url
end
end
Rails.configuration.x.external_api
# => {:timeout=>5, :url=>"https://example.org/api/path"}
Rails.configuration.x.external_api.timeout
# => 5
Rails.configuration.x.external_api.url
# => "https://example.org/api/path"
Rails.configuration.x.external_api.missing_element
# => nil
It will work anywhere in your application, including models and controllers.
How does the x.my_config.something work under the hood?
What’s the magic that allows you to define the keys in such way?
Rails.configuration.x.class
# => Rails::Application::Configuration::Custom
Let’s see if the setters and getters are implemented using method_missing
?
Rails.configuration.x.method(:method_missing).source_location
=> ["/home/rupert/.rvm/gems/ruby-2.4.1/gems/railties-5.1.4/lib/rails/application/configuration.rb", 200]
Indeed they are. Let’s see the code.
class Custom
def initialize
@configurations = Hash.new
end
def method_missing(method, *args)
if method =~ /=$/
@configurations[$`.to_sym] = args.first
else
@configurations.fetch(method) {
@configurations[method] = ActiveSupport::OrderedOptions.new
}
end
end
def respond_to_missing?(symbol, *)
true
end
end
That’s quite a simple and small implementation. If it recognizes that the method name ends with =
it sets the value. If it does not end with =
, it sets ActiveSupport::OrderedOptions.new
as the value and returns it.
Rails.configuration.x
# => #<Rails::Application::Configuration::Custom:0x000000047a42a8 @configurations={}>
Rails.configuration.x.one
# => {}
Rails.configuration.x.one.class
# => ActiveSupport::OrderedOptions
So the 1st level (x.one
, x.one=
) is managed by Rails::Application::Configuration::Custom
class and its implementation. The 2nd level (x.one.two
, x.one.two=
) is managed by ActiveSupport::OrderedOptions
.
Rails.configuration.x.one.two
# => nil
Rails.configuration.x.one.two = 2
# => 2
Let’s see what’s there.
ActiveSupport::OrderedOptions.new.method(:method_missing).source_location
=> ["/home/rupert/.rvm/gems/ruby-2.4.1/gems/activesupport-5.1.4/lib/active_support/ordered_options.rb", 39]
class OrderedOptions < Hash
alias_method :_get, :[] # preserve the original #[] method
protected :_get # make it protected
def []=(key, value)
super(key.to_sym, value)
end
def [](key)
super(key.to_sym)
end
def method_missing(name, *args)
name_string = name.to_s
if name_string.chomp!("=")
self[name_string] = args.first
else
bangs = name_string.chomp!("!")
if bangs
fetch(name_string.to_sym).presence || raise(KeyError.new("#{name_string} is blank."))
else
self[name_string]
end
end
end
def respond_to_missing?(name, include_private)
true
end
end
And that’s how it works on the 2nd level.
oo = ActiveSupport::OrderedOptions.new
# => {}
oo.foo = 1
# => 1
oo.foo
# => 1
oo.foo!
# => 1
oo.bar
# => nil
oo.bar!
#KeyError: key not found: :bar
# from (irb):13
Would you like to continue learning more?
If you enjoyed the article, subscribe to our newsletter so that you are always the first one to get the knowledge that you might find useful in your everyday Rails programmer job.
Content is mostly focused on (but not limited to) Ruby, Rails, Web-development and refactoring big, complex Rails applications.