Rails Configuration Examples

Note: this is a work in progress...

Currently, the only standard way to configure apps is via raw code. In a Rails app, this code goes somewhere in config, most commonly in either an initializer script or a yaml file. Here are some common configurations:

database.yml (default Rails database configuration file)

development:
adapter: sqlite3
database: db/development.sqlite3
pool: 5
timeout: 5000

test:
adapter: sqlite3
database: db/test.sqlite3
pool: 5
timeout: 5000

production:
adapter: sqlite3
database: db/production.sqlite3
pool: 5
timeout: 5000

authorization_rules.rb (basic declarative_authorization configuration)

authorization do
role :guest, :title => "Guest" do
description "The default role for anonymous user"
# Don't remove this or you can't signup
has_permission_on :users, :to => [:create, :read, :update, :delete]
has_permission_on :posts, :to => :read
end

role :customer, :title => "Customer" do
description "The default role for anonymous user"
# Don't remove this or you can't signup
has_permission_on :users, :to => [:create, :read, :update, :delete]
has_permission_on :posts, :to => :read
end

role :admin, :title => "Administrator" do
description "Somebody who has access to all the administration features"
has_permission_on :authorization_rules, :to => :read
has_permission_on :authorization_usages, :to => :read
has_permission_on [:users, :roles, :posts], :to => :manage
has_permission_on [:admin_dashboard, :admin_posts, :admin_tags, :admin_assets], :to => :manage
end

role :editor, :title => "Editor" do
description "Somebody who can publish and manage posts and pages as well as manage other users' posts, etc."
has_permission_on :authorization_rules, :to => :read
has_permission_on :authorization_usages, :to => :read
has_permission_on :users, :to => :create
end

role :author, :title => "Author" do
description "Somebody who can publish and manage posts and pages as well as manage other users' posts, etc."
has_permission_on :authorization_rules, :to => :read
has_permission_on :authorization_usages, :to => :read
has_permission_on :users, :to => :create
end

role :contributor, :title => "Contributor" do
description "Somebody who can write and manage their posts but not publish them"
has_permission_on :users, :to => [:show, :update]
end
end

privileges do
# default privilege hierarchies to facilitate RESTful Rails apps
privilege :manage, :includes => [:create, :read, :update, :delete]
privilege :read, :includes => [:index, :show]
privilege :create, :includes => :new
privilege :update, :includes => :edit
privilege :delete, :includes => :destroy
end

en-US.yml (english locale, excerpt from Spree)

---
en:
'no': "No"
'yes': "Yes"
5_biggest_spenders: "5 Biggest Spenders"
abbreviation: Abbreviation
access_denied: "Access Denied"
account: Account
account_updated: "Account updated!"
action: Action
alt_text: Alternative Text
actions:
cancel: Cancel
create: Create
destroy: Destroy
list: List
listing: Listing
new: New
update: Update
active: "Active"
activerecord:
attributes:
address:
address1: Address
address2: "Address (contd.)"
city: City
country: "Country"
first_name: "First Name"
first_name_begins_with: "First Name Begins With"
last_name: "Last Name"
last_name_begins_with: "Last Name Begins With"
phone: Phone
state: "State"
zipcode: "Zip Code"
checkout:
bill_address:
address1: "Billing address street"
city: "Billing address city"
firstname: "Billing address first name"
lastname: "Billing address last name"
phone: "Billing address phone"
state: "Billing address state"
zipcode: "Billing address zipcode"

Common Solutions to the Settings Problem

Each one of the above configuration "implementations" accomplishes the same goal:

Define arbitrarily nested key/value pairs that don't map to a formal object attribute

In Rails, we are used to using initializer scripts and yaml files to define very basic configuration, such as the title of a form or error messages on ActiveRecord objects. If you want to go any deeper, you have these solutions:

All of those libraries have the following attributes:

  1. Targeted at ActiveRecord. i18n is starting to support pluggable backends however.
  2. Are primarily used for simple values (integer, strings, other YAML/JSON supported values). Some of them might support Proc's, but not most.

The problem with these current implementations however is that it's still a mess to try to access/modify these settings from an interface, to associate them with different models/contexts, to apply complex/dynamic values to them, and to use them with frameworks other than ActiveRecord.

What Configuration Should Be

The perfect configuration gem will include these features:

  • Swappable data stores. Check out wycats' moneta for a start.
  • Be editable from a User Interface.
  • Be declarable from a Domain Specific Language.
  • Be lower level than, and ultimately replace, the Ruby Internationalization gem.
  • Have nestable settings.

The following terms are all describing configuration:

  • Options
  • Preferences
  • Settings
  • Configuration

... which boils down to customizable, nested, typed, persistent, key/value pairs.

Settings have Associations

Settings can be associated with another object, for instance, "site settings". Site settings would be things that aren't necessarily integral to the object Site but should nonetheless be customizable. This includes things like the number of posts to show on a page or in a feed (e.g in a blog), your google analytics id, and maybe the time zone your site is in. This means that the site has both attributes and settings. Here are sample ActiveRecord Site and Settings models:

class Site < ActiveRecord::Base
has_many :settings, :as => :configurable
accepts_nested_attributes_for :settings
end

class Setting < ActiveRecord::Base
belongs_to :configurable, :polymorphic => true
end

create_table :settings, :force => true do |t|
t.string :key
t.string :value
t.string :context
t.string :configurable_type
t.integer :configurable_id
end

create_table :sites, :force => true do |t|
t.string :title
end

You could use settings like this:

site = Site.create!(:title => "Your Blog")
site.settings << Setting.new(:key => "time_zone", :value => "Pacific Time (US & Canada)", :context => "basic settings")

You could then edit site settings in an admin panel like this:

# controller
def update
site.settings_attributes = params[:site][:settings]
site.save
end

# view
= form_for @site do |site_form|

= site_form.label :title
= site_form.text_field :title

= site_form.fields_for :settings do |setting_form|
= setting_form.label :key
= setting_form.text_field :value

= submit_tag

Settings should have Swappable Data Stores

Since settings are just key/value stores, they should be easily mappable to any database, SQL or NoSQL. Moneta has started this for NoSQL databases for simple stores (no nesting support), so that's what it would look like. Ruby i18n is a key/value store, so it should also have a mappable backend.

Settings should be Nestable

This is very important. This is what locales are in Rails (above). You should be able to have settings like this:

site:
time:
time_zone: "Pacific Time (US & Canada)"
feed:
per_page: 10
formats: rss, atom
images:
thumb:
width: 100
height: 80

... which would be accessible using something like this:

Setting.find("site.time.timezone") #=> "Pacific Time (US & Canada)"

... and updated like this:

Setting.update("site.feed" => {:per_page => 20, :formats => "rss"})
#=> [#<Setting key='site.feed.per_page' value=20>, #<Setting key='site.feed.formats' value='rss'>]

This is how Ruby i18n works. But they don't have the swappable data stores or non-string types.

Settings should be able to have any Type

If you can do the following, how would you support a Hash in Setting?

Setting.update("site.feed" => {:per_page => 20, :formats => "rss"})

There are two ways to solve this:

a. Define Setting types using a DSL (like you would define key in a MongoMapper::Document)

site do
feed do
per_page 10, Integer
formats ["rss", "atom"], Array
end
end

b. Specify a boolean in the save or update method, which says if the object should be saved as a hash or not:

Setting.update("site.feed" => {:per_page => 20, :formats => "rss"}, true)
#=> [#<Setting key='site.feed' value={:per_page => 20, :formats => "rss"}>]

... or we could just say "no hashes in settings, those are sub-settings".

Settings should have Callbacks

What if you have a google_analytics_key setting, and when it is specified, it should install Google Analytics? One solution is to handle this specific case in a controller (spree does this for mail settings). I think there should just be a SettingsController and have an after_save callback specific to the google_analytics_key setting:

site do
google_analytics_key "", String do
after_save do
analytics = GoogleAnalytics.new(self.value)
analytics.add_to_templates # ... whatever the api is for that
end
end
end

The Power of Settings

If everything above was implemented, you could do stuff like this:

site do
# menu
menu "main" do
link "/", "Home"
link "/blog", "Blog"
# link "/portfolio", "Portfolio"
link "http://lancepollard.info/", "Resume"
end

# javascripts
javascripts do

end

# stylesheets
stylesheets do

end

# feed
feed do
posts do
title "Viatropos' Feed"
author "Lance Pollard"
url c("metadata.domain")
description <<-EOF
<p class="vcard">Thoughts and musings of <a href="http://viatropos.com/" class="url fn">Lance Pollard</a>, a <span class="title">programmer</span> living in <span class="adr"><span class="locality">Berkeley</span>, <abbr class="region"title="California">CA</abbr></span>.</p>
EOF
alternate_url "#{c("metadata.domain")}/blog/"
category_url "#{c("metadata.domain")}/tags/"
icon c("icons.favicon")
logo c("icons.site_icon")

site.posts.each do |post|
next unless post.date
entry "#{c("metadata.domain")}#{post.path}",
:title => post.title,
:description => post.read_description(200),
:created_at => post.date,
:updated_at => post.date,
:author => "Lance Pollard",
:categories => post.tags,
:content => post.render
end
end
end

# sitemap
sitemap do
stylesheet "/stylesheets/sitemap.xsl"

site.posts.each do |post|
link "#{c("metadata.domain")}#{post.path}", :changes => "weekly"
end
end

# templates
templates do
contact
error
overview
pages
large_portfolio "portfolio/large"
medium_portfolio "portfolio/medium"
small_portfolio "portfolio/small"
posts
end

# layouts
layouts do
full do
description "A full width layout, mainly for text"
areas :top, :bottom, :left, :right, :header, :footer, :body
template :pages
end
left do
description "Side bar on the right, content on the left"
template :pages
areas :top, :bottom, :right, :header, :footer, :body
end
right do
description "Side bar on the left, content on the right"
template :pages
areas :top, :bottom, :left, :header, :footer, :body
end
blog do
description "Blog Layout"
template :posts
areas :right, :top, :bottom, :header, :footer, :body
end
page do
description "Page Layout"
template :pages
areas :right, :top, :bottom, :header, :footer, :body
end
contact do
description "Contact Layout"
template :contact
areas :top, :bottom, :header, :footer, :body
end
overview do
description "Overview Layout"
template :overview
areas :top, :bottom, :header, :footer, :body
end
large_portfolio do
description "Large Portfolio Layout"
template :large_portfolio
areas :top, :bottom, :header, :footer, :body
end
medium_portfolio do
description "Medium Portfolio Layout"
template :medium_portfolio
areas :top, :bottom, :header, :footer, :body
end
small_portfolio do
description "Small Portfolio Layout"
template :small_portfolio
areas :top, :bottom, :header, :footer, :body
end
end

# widgets
widgets do
connect do
title :string
facebook :string
twitter :string
my_space :string
linked_in :string
stack_overflow :string
github :string
email :string
end
image do
title :string
url :string
alt_text :string
image_title :string
description :string
image_align :string
link_url :string
end
links do
title "Blog Roll"
end
login do
title "Login"
google true
facebook true
twitter true
open_id true
end
meta do
title "Meta"
end
search do
title "Search"
end
social do
title "Social"
end
tag_cloud do
title "Tag Cloud"
end
text do
title "Random Text"
body :string
end
map do
title "Map"
coordinates ""
end
end

# settings
settings do
asset do
thumb do
width 100, :tip => "Thumb's width"
height 100, :tip => "Thumb's height"
end
medium do
width 600, :tip => "Thumb's width"
height 250, :tip => "Thumb's height"
end
large do
width 600, :tip => "Large's width"
height 295, :tip => "Large's height"
end
end
seo do
pretty_markup true
end
authentication do
use_open_id true
use_oauth true
email "[email protected]"
password "martini"
end
front_page do
slideshow_tag "slideshow"
slideshow_effect "fade"
end
page do
per_page 10
feed_per_page 10
formats "markdown", :options => ["textile", "markdown", "plain", "html", "haml"]
end
people do
show_avatars true
default_avatar "/images/missing-person.png"
end
site do
title "Martini", :tooltip => "Main Site Title!"
tagline "Developer Friendly, Client Ready Blog with Rails 3"
keywords "Rails 3, Heroku, JQuery, HTML 5, Blog Engine, CSS3"
copyright "© 2010 Viatropos. All rights reserved."
domain "haveamartini.com"
menu :type => :integer,
:value => lambda { Post.roots },
:options => lambda { Post.roots.map {|p| [p.title, p.id]} }
date_format "%m %d, %Y"
time_format "%H"
week_starts_on "Monday", :options => ["Monday", "Sunday", "Friday"]
language "en-US", :options => ["en-US", "de"]
touch_enabled true
touch_as_subdomain false
google_analytics "UA-15601175-11"
mixpanel "c0e8c5b0cba24fce24b915292ea74753"
logo "/images/logo-thumb.png"
cache_duration 60
permalink do
default "/:title"
date "/:date/:title"
numeric "/:id"
custom ""
end
teasers do
disable false
left :type => :integer,
:value => lambda { Post.first },
:options => lambda { Post.tree {|p| [p.title, p.id]} }
right :type => :integer,
:value => lambda { Post.first },
:options => lambda { Post.tree {|p| [p.title, p.id]} }
center :type => :integer,
:value => lambda { Post.first },
:options => lambda { Post.tree {|p| [p.title, p.id]} }
end
main_quote :type => :integer,
:value => lambda { Post.first },
:options => lambda { Post.tree {|p| [p.title, p.id]} }
end
social do
facebook "http://facebook.com/viatropos"
twitter "http://twitter.com/viatropos"
email "[email protected]"
end
s3 do
key "my_key"
secret "my_secret"
bucket "ilove4d-test"
permalink ":attachment/:id/:style.:extension"
end
end
end

Introducing Cockpit - The Missing Rails Settings Gem

Cockpit aims to solve all of these problems:

  • Swappable Datastores
  • Nestable Settings
  • Settings can be associated to any other object
  • Settings can be of any Type
  • Settings can be defined in YAML, a DSL, or plain Ruby
  • Settings can be (easily) edited from a User Interface

Cockpit is a layer above Moneta, since it is already pretty far along and implements a clear, low-level, api.

Here are a few examples...

MongoMapper::Document Settings

Say you have a Post model:

class Post
include MongoMapper::Document
many :settings, Setting
key :title
end

... and want it to have arbitrary settings. At a very basic level, you can create runtime settings like you normally would:

# runtime settings
post = Post.create!(:title => "A Post!")
post.settings << Setting.new(:key => "time_zone", :value => "Pacific Time (US & Canada)", :context => "basic settings")

You could also, more preferrably, pre-define the settings you want your post to have and give it default values:

# dsl settings (e.g config/settings.rb)
post do
settings do
time_zone "Pacific Time (US & Canada)"
end
end

# then in your code somewhere...
post = Post.create!(:title => "A Post!")
post.settings
#=> [#<Setting key='time_zone' value=>'Pacific Time (US & Canada)']
post.settings.time_zone = "Hawaii"
#=> [#<Setting key='time_zone' value=>'Hawaii']
post.settings.save

Uniform Configuration Interface

The end result of building this gem would be that all application settings would be editable via code and via a UI. This is powerful.

Cockpit currently only supports ActiveRecord, but I will be implementing MongoDB next because it's much better in my opinion to store this kind of unstructured data in a NoSQL database than a SQL one. I aim to have it do all of what i18n does but more low level/abstract, so that all i18n is is a translation library built on top of a Ruby key/value library.

Resources

Some other interesting projects and related resources: