1

I have a homework problem to create a simple DSL configuration for Ruby.

The problem is in method_missing. I need to print out values of keys, but they're printing out automaticaly, not by command.

init.rb:

require_relative "/home/marie/dsl/store_application.rb"

config = Configus.config do |app|
  app.environment = :production

  app.key1 = "value1"
  app.key2 = "value2"

  app.group1 do |group1|
    group1.key3 = "value3"
    group1.key4 = "value4"
  end
end

store_application.rb:

class Configus
  class << self
    def config
      yield(self)
    end

    #    attr_accessor :environment,
    #                  :key1,
    #                  :key2,
    #                  :key3,
    #                  :key4

    def method_missing(m, args)
      puts args
    end

    def group1(&block)
      @group1 ||= Group1.new(&block)
    end
  end

  class Group1
    class << self
      def new
        unless @instance
          yield(self)
        end
        @instance ||= self
      end

      # attr_accessor :key1,
      #               :key2,
      #               :key3,
      #               :key4

      def method_missing(m, *args)
        p m, args
      end
    end
  end
end

Ruby's init.rb output:

marie@marie:~/dsl$ ruby init.rb 
production
value1
value2
:key3=
["value3"]
:key4=
["value4"]

The problem is that the values are printing automatically, I need to print them out using:

config.key1         => 'value1'
config.key2         => 'value2'
config::Group1.key3 => 'value3'
config::Group1.key4 => 'value4'
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
  • I think the approach of using a method that yields in order to assign values is overly complex in this case. I suggest simplifying the code; often that by itself eliminates the problem. Also, for printing out configuration values, I find that the simplest way is to implement a `to_h` method that creates a hash with the values, and then using JSON, YAML, or awesome_print to output it nicely. – Keith Bennett Dec 15 '19 at 18:11

1 Answers1

1

There are several things in your implementation that need to be fixed to match your expectations:

1) config class method returns the result of the block execution, so in your example the config variable contains Configus::Group1, not Configus as you probably expect.

2) method_missing now behaves in the very same way regardless of the method name. But it is quite clear that you expect different behavior for setters and getters.

So a naive (and dirty) fix could look like the following:

class Configus
  class << self    
    def config
      yield(self) if block_given?
      self
    end

    def method_missing(method_name, *args)
      @config_keys ||= {}

      if method_name.to_s.end_with?('=')
        @config_keys[method_name.to_s[0..-2].to_sym] = args
      elsif @config_keys.key?(method_name)
        @config_keys[method_name]
      else
        super
      end
    end

    # ...
  end

  # ...
end

(the same applies to the Group1, but I believe you got the idea of how to fix it too)

There is one more practical problem with your DSL, though: the support for nested setting is hard-coded and this makes it non-flexible. You cannot build nested hierarchies this way, for example, and to introduce new nested group you have to change the class definition (add method(s)). There are plenty of ways to fix this in Ruby, for example, we could use OpenStruct that does a lot of method_missing magic under the hood and simplifies the code a bit because of that. Dirty example:

require "singleton"

class Configus
  include Singleton

  class ParamSet < OpenStruct
    def method_missing(method_name, *args)
      # Naive, non-robust support for nested groups of settings
      if block_given?
        subgroup = self[method_name] || ParamSet.new
        yield(subgroup)
        self[method_name] = subgroup
      else
        super
      end
    end
  end

  def self.config
    yield(self.instance.config) if block_given?
    self.instance
  end

  def method_missing(method_name, *args)
    config.send(method_name, *args) || super
  end

  def config
    @config ||= ParamSet.new
  end
end

Now you can nest the settings, for example

config = Configus.config do |app|
  app.environment = :production

  app.key1 = "value1"
  app.key2 = "value2"

  app.group1 do |group1|
    group1.key3 = "value3"
    group1.key4 = "value4"
    group1.group2 do |group2|
      group2.key5 = "foo"
    end
  end
end

and then

config.key1 #=> "value1"
config.group1.key3 #=> "value3"
config.group1.group2.key5 #=> "foo"

P.S. One more thing to mention: the rule of thumb is to define the appropriate respond_to_missing? each time you play with method_missing (at least for production-grade code)...

Konstantin Strukov
  • 2,899
  • 1
  • 10
  • 14
  • But how to not use that app or group1 parameter in block? Like that: config = Configus.config :production do env :production do key1 "value1" key2 "value2" group1 do key3 "value3" key4 "value4" end end –  Dec 17 '19 at 07:02