Creating configuration files for Ruby programs
Introduction
There are different ways to keep configuration information outside of ruby program. You can use .ini, .xml, .properties, .json, .yml formats to achieve it.
You can find some issues with this approach though. In case when you need to evaluate one property based on value of another property, it could be very difficult or almost impossible.
Let’s show some examples of external configuration. In xml format it could look this way:
<?xml version="1.0" encoding="UTF-8"?>
<properties>
<property name="property1" value="value1"/>
<property>
<name>property2</name>
<value>value2</value>
</property>
<property name="property3" value="#{property1}123"/>
</properties>
Question: how to implement reference to another property, e.g. properties.property1 for properties.property3?
Another example in json format:
{
"property1": "value1",
"property2": {
"property21": "value21"
},
"property3": "#{property1}/123"
}
We can ask same question here.
Let’s take a look at possible solutions.
Using text interpolation
You can use some form of text interpolation to substitute such values based on the knowledge of the context. Look at text_interpolator gem and article about how to use this gem for further details. In short, it uses simple ruby trick:
env = {var1: 'some value 1', var2: 'some value 2'}
template = "We have var1: %{var1} and var2: %{var2}."
result = template % env
puts result # We have var1: some value 1 and var2: some value 2.
With this technick you can use multi-level properties, e.g.:
{
"tomcat": {
"home": "/usr/local/Cellar/tomcat7/7.0.56/libexec",
"deploy_dir": "#{tomcat.home}/webapps"
},
"jboss": {
"home": "/usr/local/Cellar/jboss-as5/5.1.0GA/libexec",
"deploy_dir": "#{jboss.home}/server/default/deploy"
}
Using ruby language for describing configuration
Another idea is to use Ruby fragment (piece of code) to represent the configuration. In such a way you don’t have to a) invent yet another language to represent configuration and b) you can use language’s expressiveness to have text interpolation at the moment when you really need it. For example:
# .test_config
rails_env = "production"
ant_home = ENV['ANT_HOME'] || "#{ENV['HOME']}/apache-ant-1.8.3"
project_name = "web_app_builder_test"
gems_to_reject = %w(bundler)
groups_to_reject = %w(test)
groups_to_reject << 'development' unless %w(development staging).include? rails_env.to_sym
author = "Alexander Shvets"
templates_dir = "config/templates"
You can read this file and then convert it to a hash with the help of ruby eval method. Complete code is implemented as part of meta_methods gem:
require 'meta_methods'
content = File.read(".test_config")
hash = MetaMethods::Core.instance.block_to_hash content
Conclusion
All technics described above are implemented as separate config-file gem. You can use it to read from 3 most popular formats.
- from yaml:
require 'config_file'
config_file = ConfigFile.new
config = config_file.read "spec/config/test_config.yaml"
puts config
- from json:
config = config_file.read "spec/config/test_config.json"
puts config
- from ruby:
config = config_file.read "spec/config/.test_config", ".rb"
puts config
or register your own configuration format. Below is support for xml format:
require 'nokogiri'
require 'active_support/core_ext/hash'
require 'config_file/config_file'
class ConfigType
class Xml
ConfigFile.register(self)
def self.extensions
[".xml"]
end
def read file_name
doc = Nokogiri::XML(File.read(file_name))
HashWithIndifferentAccess.new Hash.from_xml(doc.to_s)
end
end
end
and then use it:
config_file = ConfigFile.new
config = config_file.read "spec/config/test_config.xml"
puts config