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'
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)...