1

I have a file that uses a DSL for complex configuration, part of that larger DSL is a name/value settings DSL.

Whenever I use a setting called name, I get an error.

Failure/Error: name 'SomeName' ArgumentError: wrong number of arguments (given 1, expected 0)

I'm looking for a solution to this edge-case.

Any instance of the SettingsDsl can have a unique name to identify itself, this is an optional parameter for the initializer.

example

main_dsl = MainDsl.new do 
  settings do
    rails_port        3000
  end
  settings :key_values do
    rails_port        3000
  end
end

This would create two instances of SettingsDsl, one with a name of :settings and the other with a name of :key_values

This is the code for MainDsl and SettingsDsl

class MainDsl
  attr_reader :meta_data

  def initialize(&block)

    @meta_data = HashWithIndifferentAccess.new

    if block_given?
      self.instance_eval(&block)
    end
  end

  def settings(name = nil,&block)
    settings = SettingsDsl.new(@meta_data, name, &block)

    puts setting.name

    settings
  end

end

class SettingsDsl

  attr_reader :name

  def initialize(data, name = nil, &block)
    @data = data
    @name = name ||= :settings

    @data[name] = {}

    self.instance_eval(&block) if block_given?
  end

  def method_missing(key, *args, &block)
    @data[@name][key] = args[0]
  end

  def get_value(key)
    @data[@name][key]
  end

end

All works well until I use an internal key/value pair called name

I use method_missing to find new keys and store those values into a Hash, but in the case of name, there is already an attr_reader and this uses a slightly different signature and causes an argument error.

main_dsl = MainDsl.new do 
  settings do
    rails_port        3000
    name              'SomeName' # Intercept the error for this and store 'SomeName' into @data[@name]['name']
    another_property  'Something Else'
  end
  settings :more do
    hello 'world'
  end
end

Failure/Error: name 'SomeName' ArgumentError: wrong number of arguments (given 1, expected 0)

The problem happens because internally there is a name attribute for the settings group, e.g.

SettingsDsl.new('name_goes_here')
# and gets stored in @name
# and is accessible via attr_reader :name
# which creates a read-only method called name. e.g.
SettingsDsl.new('name_goes_here').name

I would like to intercept the ArgumentError for this one method call and handle this edge-case appropriately

Final output could then look like

{
  'settings': {
    'rails_port': 3000,
    'name': 'SomeName',
    'another_property': 'Something Else'
  },
  'more': {
    'hello': 'world'
  }
}
1
  • 1
    What about having a proxy object that you expose in that DSL block instead of the raw object directly? You can map method_missing calls to the target mutator. Commented Aug 18, 2019 at 0:17

1 Answer 1

3

You can get rid of the attr_reader :name and implement the edge-case logic yourself:

def name(*args)
  args.empty? ? @name : @data[@name][:name] = args[0]
end

Here is a complete example:

stackoverflow-57540225.rb:

require 'active_support/hash_with_indifferent_access'
require 'json'

class MainDsl
  attr_reader :meta_data

  def initialize(&block)
    @meta_data = HashWithIndifferentAccess.new

    if block_given?
      self.instance_eval(&block)
    end
  end

  def settings(name = nil,&block)
    settings = SettingsDsl.new(@meta_data, name, &block)
  end
end

class SettingsDsl
  def initialize(data, name = nil, &block)
    @data = data
    @name = name ||= :settings

    @data[name] = HashWithIndifferentAccess.new

    self.instance_eval(&block) if block_given?
  end

  def name(*args)
    args.empty? ? @name : @data[@name][:name] = args[0]
  end

  def method_missing(key, *args, &block)
    @data[@name][key] = args[0]
  end

  def get_value(key)
    @data[@name][key]
  end
end

main_dsl = MainDsl.new do 
  settings do
    rails_port        3000
    name              'SomeName'
    another_property  'Something Else'
  end
  settings :more do
    hello 'world'
  end
end

puts main_dsl.meta_data.to_json

Result:

$ ruby stackoverflow-57540225.rb | jq .
{
  "settings": {
    "rails_port": 3000,
    "name": "SomeName",
    "another_property": "Something Else"
  },
  "more": {
    "hello": "world"
  }
}
Sign up to request clarification or add additional context in comments.

8 Comments

You could also make an attr_-style generator for this too with a bit of meta-programming.
@Amit - thanks for the idea, but I still need to be able to do an assignment to [at]name. [at]name is an optional property of the SettingsDsl that is set during the constructor, while the name method within the DO END maps to a hash like this [at]data[[at]name]['name']. The problem from the outside world they at the same level the only [at]name can only be set via the constructor and [at]data[[at]name] can only be set from within the block.
Perhaps you could include exactly what kinds of method calls you'd like to be able to make, and what the expected behaviour of those calls should be, in your question? You haven't said exactly what settings does inside the MainDsl.new do ... end block, but guessing it includes some call to SettingsDsl.new, the suggestion I've provided doesn't result in the error you mention in your above post and seems to behave as you're expecting. If there are additional requirements could you explicitly enumerate them?
I've added a new section to the description, I hope that clears things up
Could you please give me an example specifically of what the code I have above does vs what you’d expect it to do? Unfortunately I’m still not seeing the problem
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.