How would you build a single GEM configuration that can be extended automatically if additional but optional child GEMS are included with a Ruby Application.
I am looking either for a code pattern or an existing GEM that can provide an extendable configuration system for an application I am building.
Scenario
Let's say you have a communication app that can send/receive messages and it uses some general configuration.
gem 'communication-app'
You can install any number of providers that can send/receive via different platforms.
gem 'communication-app-facebook'
gem 'communication-app-twitter'
gem 'communication-app-sms'
gem 'communication-app-email'
Each provider needs its own custom configuration. These should hang off the main configuration and can have a custom configuration graph but only be available if the plugin is installed.
Default Configuration Usage
CommsApp.configure do |config|
config.some_setting2 = 'customised setting'
end
puts CommsApp.configuration.some_setting1
puts CommsApp.configuration.some_setting2
# --> some default value 1
# --> customised setting
Example Code
module CommsApp
class << self
attr_writer :configuration
def configuration
@configuration ||= Configuration.new
end
def configure
yield(configuration)
end
end
class Configuration < BaseConfiguration
attr_accessor :some_setting1
attr_accessor :some_setting2
def initialize
super
@some_setting1 = 'some default value 1'
@some_setting2 = 'some default value 2'
end
end
end
If I try to access configuration for a child GEM that is not installed, I would expect to see some sort of error. eg.
puts CommsApp.configuration.twitter.access_token
# raises NoMethodError, "unknown method :twitter"
I have experimented with a class method on the base configuration that would allow me to attach child configuration properties for a new configuration class.
module CommsApp
class BaseConfiguration
class << self
# Attach a child configuration with it's own settings to a parent configuration
#
# @param [Class] klass_child what class would you like as the child
# @param [Class] klass_parent what class would you like to extend with a new child configuration
# @param [Symbol] accessor_name what is the name of the accessor that you are adding
def attach_to(klass_child, klass_parent, accessor_name)
# Create a memoized getter to an instance of the attaching class (:klass_child)
#
# def third_party
# @third_party ||= ThirdPartyGem::Configuration.new
# end
klass_parent.send(:define_method, accessor_name) do
return instance_variable_get("@#{accessor_name}") if instance_variable_defined?("@#{accessor_name}")
instance_variable_set("@#{accessor_name}", klass_child.new)
end
end
end
end
end
Extended Configuration Usage
This is how the extend configuration could be used.
class ApplicationConfig < CommsApp::Configuration
attach_to(CommsApp::TwitterProvider::Configuration, self, :twitter)
attach_to(CommsApp::FacebookProvider::Configuration, self, :fb)
end
CommsApp.configuration = ApplicationConfig.new
CommsApp.configure do |config|
config.some_setting2 = 'customized setting'
config.twitter.access_token = '123'
config.fb.user_name = 'xmen'
config.fb.account_id = 'xmen123'
end
This technique works ok, but it does require a custom class to be created and configured and I want this to happen automagically if (and only if) the provider GEMs are present.
I know I can figure out how to do this, but I am wondering if there is already a pattern or GEM that is solving this particular problem.