Ruby Flexible Configuration across GEMS

Viewed 30

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.

0 Answers
Related