rubyrefinements

Configurable refinements in ruby


I'd like to patch parts of an existing library with a config object:

module Library
  class A
  end

  class B
  end
end

module Config
  def config(&block)
    @config ||= block&.call
  end
end

module Patch
  refine Library::A.singleton_class do
    extend Config
    config { "my for A" }
  end

  refine Library::B.singleton_class do
    extend Config
    config { "my for B" }
  end
end

Library::A should have some predefined config and Library::B should have its own config. Unfortunately, this throws an error:

using Patch
Library::A.config
=> undefined method `config' for Library::A:Class (NoMethodError)

I guess I don't understand how refinements work in this case. But is there a way to achieve something like this?


Solution

  • here is my solution, basically i include Config to the Patch to be able to define configurations at class level, then include the Patch itself to the refined classes (Library::A and Library::B) to be able to access the defined configurations.

    module Config
      def self.included(base)
        class << base
          def refinement(clazz, patch=self, &block)
            $predefined_configs ||= Hash.new
            $predefined_configs[clazz] ||= Hash.new
            block&.call($predefined_configs[clazz])
            refine clazz.singleton_class do
              include patch
            end
          end
        end
      end
    
      def config
       $predefined_configs[self].tap do |_config|
          # reuse common configs
          _config.merge!({
            lang: "ruby"
          })
       end
      end
    end
    

    then

    module Library
      class A; end
    
      class B; end
    end
    
    module Patch
      include Config
    
      refinement(Library::A) do |_config|
        _config[:name] = "my for A"
      end
    
      refinement(Library::B) do |_config|
        _config[:name] = "my for B"
      end
    end
    
    class LoadConfig
      using Patch
      puts Library::A.config # {:name=>"my for A", :lang=>"ruby"}
    end
    
    puts Library::A.config # error
    
    using Patch
    puts Library::B.config # {:name=>"my for B", :lang=>"ruby"}