Search code examples
rubyjsonblock

Ruby - (JSON) with methods to block


I have a JSON with the following construct.

{
  "methods": [
    {
      "method": "method_name",
      "argument": "argument_value",
      "options_key": "options_value",
      "another_options_key": "options_value"
    },
    {
      "method": "method_name",
      "argument": "argument_value",
      "options_key": "options_value",
      "another_options_key": "options_value"
    }
  ]
}

This JSON gets parsed and the arguments are found with:

def parse_json(json)
    methods = JSON.parse(json, , :symbolize_names => true)
    methods.each do |options|
      pass_method(options)
    end
end

def pass_method(options)
  argument_names = self.class.instance_method(options[:method].to_sym).parameters.map(&:last)
  args = argument_names.map do |arg| 
    if arg == :options
      options
    else
      options[arg] || ''
    end
  end

  self.send(options[:method], *args)
end

Now I would like to pass blocks to those methods. The JSON would look like:

{
  "methods": [
    {
      "method": "method_name",
      "argument": "argument_value",
      "options_key": "options_value",
      "another_options_key": "options_value",
      "block": [
        {
          "method": "method_name",
          "argument": "argument_value",
          "options_key": "options_value",
          "another_options_key": "options_value"
        },
        {
          "method": "method_name",
          "argument": "argument_value",
          "options_key": "options_value",
          "another_options_key": "options_value"
        }
      ]
    }
  ]
}

How can I make this work? So I can pass blocks to the methods and have sub-methods executed in the block?


Solution

  • Well, first of all I think your current approach of passing arguments based on the names of the method parameters is a bit strange. Instead, why not pass arguments as an array, similar to the form required by Object#send?

    {
      "method": "push",
      "arguments": [
        1, "some_text", {"key": "value"}, ["array"]
      ]
    }
    

    This greatly simplifies the code, and makes it possible to pass keyword arguments using a hash.

    As for how to pass blocks to methods, you can do that by constructing a Proc object, and passing that to the send method using the &block syntax:

    method = :inspect
    args = [1, "some_text", {"key": "value"}, ["array"]]
    block = proc{|x| x.send(method, *args) }
    some_object.send(:map!, &block)
    

    Putting all these ideas together, we arrive at the following solution:

    json = <<-JSON
    {
      "methods": [
        {
          "method": "push",
          "arguments": [
            1, "some_text", {"key": "value"}, ["array"]
          ]
        },
        {
          "method": "delete",
          "arguments": ["some_text"]
        },
        {
          "method": "map!",
          "arguments": [],
          "block": [
            {
              "method": "to_s",
              "arguments": []
            } 
          ]
        }
      ]
    }
    JSON
    
    def to_call_proc(method)
      method_name = method['method']    || ''
      arguments   = method['arguments'] || []
      block = to_multi_call_proc(method['block']) if method.has_key? 'block'
    
      if block
        proc{|x| x.public_send(method_name, *arguments, &block) }
      else
        proc{|x| x.public_send(method_name, *arguments) }
      end
    end
    
    def to_multi_call_proc(methods)
      call_procs = methods.map(&method(:to_call_proc))
      last_call_proc = call_procs.pop
      proc do |x|
        call_procs.each{|call_proc| call_proc.call(x)}
        last_call_proc.call(x) if last_call_proc
      end
    end
    
    def call_methods(receiver, methods)
      to_multi_call_proc(methods).call(receiver)
    end
    
    require 'json'
    
    a = []
    call_methods(a, JSON.parse(json)['methods'])
    p a
    

    Result:

    ["1", "{\"key\"=>\"value\"}", "[\"array\"]"]