如何使用 Ruby 元编程使链式点符号适用于嵌套哈希键

How to make a chained dot notation work for nested hash keys using Ruby Metapraogramming

我正在编写一个函数 from scratch,它在构造函数中接受散列。请注意,在 Ruby 中,构造函数是 class 的一种特殊方法,每当创建 class 的实例时都会自动调用该方法。但是,我的一些 RSpec 测试没有通过或无法运行 because I couldn't implement it in the code

代码

class Config
  attr_accessor :a1, :a2, :a3, :a4

  def initialize(args)
    if args.is_a? Hash
      args.each do |k, v|
        instance_variable_set("@#{k}", v) unless v.nil?
      end
    else
      raise ArgumentError, "Invalid argument. Supply a Hash instead of #{args.class}"
    end
  end

  def method_missing(method_name, *args, &block)
    return nil if method_name.empty?

    if args.include?(method_name.to_sym)
      send(args[method_name.to_sym], *args, &block)
    else
      raise ArgumentError, "Method `#{method_name}` doesn't exist."
    end

    super
  end

  def respond_to_missing?(method, include_private = false)
    args.respond_to?("get_#{method}_info") || super
  end
end

rspec

describe Config do
  conf = Config.new({a1: 'test', 'a2' => {'a3' => [20, 21]}, a4: {}})

  it 'should' do
    expect(conf.a2.a3).to eq([20, 21])
  end
end

我尝试检查我的代码 运行 puts conf.a2.a3,我得到:

Uncaught exception: undefined method `a3' for {"a3"=>[20, 21]}:Hash

如果有人能对此提供帮助,我会很高兴。它适用于 conf.a1conf.a4,但我当然不知道我如何才能使它响应使用点符号的链接,例如 conf.a2.a3 和 return 值。如果您对我的解决方法有其他方法,请告诉我方法。

谢谢

您的测试无效,因为 conf.a2 return 是一个哈希(不响应消息 a3)。我不能确切地说出你想要完成什么,但看起来你可以通过确保每当你 return 一个本应是散列的值时,该值成为你的一个实例,从而使你的测试通过配置 class.

有一点很奇怪,您已经为平面名称列表定义了 attr_accessors,但是您以结构​​化方式访问名称,而不是您定义的平面列表。

如果您想以结构化的方式访问您的配置,这样的方式怎么样?尽量不要偏离你所拥有的太远:

class Config
  def initialize(args)
    if args.is_a? Hash
      args.each do |k, v|
        next if v.nil?

        if v.is_a?(Hash)
          instance_variable_set("@#{k}", Config.new(v))
        else
          instance_variable_set("@#{k}", v)
        end
      end
    else
      raise ArgumentError, "Invalid argument. Supply a Hash instead of #{args.class}"
    end
  end

  def method_missing(name, *args)
    if name.to_s.end_with?('=')
      instance_variable_set("@#{name.to_s.sub(/=$/, '')}", *args)
    else
      instance_variable_get("@#{name}", *args)
    end
  end
end