yaks/lib/yaks/configurable.rb

Summary

Maintainability
A
1 hr
Test Coverage
module Yaks
  # A "Configurable" class is one that keeps a configuration in a
  # separate immutable object, of type class::Config. say you have
  #
  #     class MyMapper < Yaks::Mapper
  #       # use yaks configuration DSL in here
  #     end
  #
  # The links, associations, etc, that you set up for MyMapper, will
  # be available in MyMapper.config, which is an instance of
  # Yaks::Mapper::Config.
  #
  # Each configuration step, like `link`, `has_many`, will replace
  # MyMapper.config with an updated version, discarding the old
  # config.
  #
  # By extending Configurable, a number of "macros" become available
  # to describe the DSL that subclasses can use. See the docs for
  # `def_set`. `def_forward`, and `def_add`.
  module Configurable
    attr_accessor :config

    def self.extended(child)
      child.config = child::Config.new
    end

    def inherited(child)
      child.config = config
    end

    # Create a DSL method to set a certain config property. The
    # generated method will take either a plain value, or a block,
    # which will be captured and stored instead.
    def def_set(*method_names)
      method_names.each do |method_name|
        define_singleton_method method_name do |arg = Undefined, &block|
          if arg.equal?(Undefined)
            unless block
              raise ArgumentError, "setting #{method_name}: no value and no block given"
            end
            self.config = config.with(method_name => block)
          else
            if block
              raise ArgumentError, "ambiguous invocation setting #{method_name}: give either a value or a block, not both."
            end
            self.config = config.with(method_name => arg)
          end
        end
      end
    end

    # Forward a method to the config object. This assumes the method
    # will return an updated config instance.
    #
    # Either takes a list of methods to forward, or a mapping (hash)
    # of source to destination method name.
    def def_forward(mappings, *names)
      if mappings.instance_of? Hash
        mappings.each do |method_name, target|
          define_singleton_method method_name do |*args, &block|
            self.config = config.public_send(target, *args, &block)
          end
        end
      else
        def_forward([mappings, *names].map{|name| {name => name}}.inject(:merge))
      end
    end

    # Generate a DSL method that creates a certain type of domain
    # object, and adds it to a list on the config.
    #
    #     def_add :fieldset, create: Fieldset, append_to: :fields
    #
    # This will generate a `fieldset` method, which will call
    # `Fieldset.create`, and append the result to `config.fields`
    def def_add(name, options)
      old_verbose, $VERBOSE = $VERBOSE, false # skip method redefinition warning
      define_singleton_method name do |*args, &block|
        defaults = options.fetch(:defaults, {})
        klass    = options.fetch(:create)

        if args.last.instance_of?(Hash)
          args[-1] = defaults.merge(args[-1])
        else
          args << defaults
        end

        self.config = config.append_to(
          options.fetch(:append_to),
          klass.create(*args, &block)
        )
      end
    ensure
      $VERBOSE = old_verbose
    end
  end
end