Rails Guide


Environment-specific Settings

Default approach: Use an initializer!

Especially if a gem or library already provides a configuration interface (e.g. ActionMailer, CarrierWave, …), you should just do the configuration right there with Ruby without introducing a second configuration layer (e.g. with a .yml file) above it.

# config/intializers/foo.rb
if Rails.env.production?
  ...
else
  ...
end

If you build your own “module/library” you should should implement a configuration interface for that “module/library”.

Advantages:

  • easy to find configuration (Where is the config for ‘foo’? –> initializers/foo.rb)
  • configuration for all environments of one lib withinin a couple of lines of code
  • more flexible than .yml, because it’s Ruby

Exception: configuration used by different “libs”

It can make sense to introduce another configuration level above the configuration of your lib or gem, if configuration options need to be used by different gems or in completely different parts of the app.

Example: The Google Analytics account id of an app is used: 1) in the application layout view, 2) in a tracking gem (e.g. ga_events), 3) and in a gem retrieving/displaying metrics from GA. This is a good usecase for storing the configuration in a separate place.

However, it’s better to simple use the configuration code that already comes with rails instead of implementing your own .yml configuration loader code.

Here is an example for setting a global default configuration value and overriding it only for the production enviroment, without using any non-Rails code:

config/application.rb:

AppName::Application.configure do
  config.google_analytics = ActiveSupport::OrderedOptions.new
  config.google_analytics.account_id = 'UA-12345-23' # dev & staging acccount
end

config/environments/production.rb:

AppName::Application.configure do
  congfig.google_analytics.account_id = 'UA-12345-1' # production account
end

Infrastructure configuration

One other important aspect concerning app configuration, is that configuration concerning internal infrastructure parts of your architecture (= stuff & services that you control), should not live inside your apps. This type of configuration should be generated by your provisioning tools (=> CHEF) instead, so that they will be updated when something changes. Examples for such parts are database server addresses, interal API endpoints, etc.

This does not apply to external parts of the infrastructure, e.g. address of your Redis-to-go server, Sendgrid, S3, etc.


Verify time zone and locale settings

Many applications allow the user to change locale and time zone settings. This is often handled by providing a url parameter or by setting a cookie value. But one has to be aware, that the user might modify those settings and therefore might brake the app by providing malicious data. In order to check the data, we can validate the input before setting the value:

# Only allow supported time zones
def valid_time_zone_or_nil(zone)
  zone = zone.to_s
  ActiveSupport::TimeZone::MAPPING.values.include?(zone) ? zone : nil
end

# Only allow supported locales
def valid_locale_or_nil(locale)
  I18n.available_locales.map(&:to_s).include?(locale.to_s) ? locale : nil
end

You can use this in you application_controller.rb like this:

around_filter :set_timezone
before_filter :set_locale

def set_locale
  I18n.locale = valid_locale_or_nil(params[:locale]) || I18n.default_locale
end

def set_time_zone(&block)
  time_zone = valid_time_zone_or_nil(params[:time_zone]) || 'Europe/Berlin'
  Time.use_zone(time_zone, &block)
end

Handle a model attribute with a set of allowed values (incl. dropdown & i18n)

Use case: We have a model (e.g. User) with an attribute (e.g. gender that can take one of the values from a certain finite set of values (e.g. 'female', 'male', 'neutral')

In the Model:

class User
  GENDER_VALUES = %w(female male neutral)
  validates :gender, inclusion: { in: GENDER_VALUES }
end

In i18n:

de:
  activerecord:
    attributes:
      user:
        gender: 'Geschlecht'
    values: # this is not rails standard, but follows their pattern
      user:
        gender:
          female: 'weiblich'
          male: 'männlich'
          neutral: 'unentschieden'

In the decorator:

class UserDecorator
  def gender_options_for_select
    options = User::GENDER_VALUES.map do |value|
      [h.t(value, scope: "activerecord.values.user.gender"), value]
    end
    h.options_for_select options, self.gender
  end
end

In the view

= form_for @user
  = f.select :gender, @user.gender_options_for_select