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:
- Targeted at ActiveRecord. i18n is starting to support pluggable backends however.
- 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
|
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: