Update a value in struct which previously unknown, easy way?

following is a example:

require "json"

struct Foo
  include JSON::Serializable

  def initialize(@value1, @value2)
  end

  property value1 : Int32
  property value2 : Int32
end

foo = Foo.new(value1: 1, value2: 1)

# I don't know which property that the user passed in to update in advance.

# We assume the user passed in value save as a variable `'key = "value1"`

key = "value1"

# so, I have to convert it into a hash at first, then update it use `#[]=`

h = Hash(String, Int32).from_json(foo.to_json)

h[key] += 1

# Then, convert it back.

pp! Foo.from_json(h.to_json) # Foo(@value1=2, @value2=1)

Instead of convert into a JSON then convert it back, is there a easier way to do this? is there a module or shard add #[], #[]= method into Struct?

I made a simple attempt to implement the get_var and set_var methods.

class Dispatcher
  property foo : Int32? = 5
  property bar : Int32? = 12

  def initialize
  end

  def set_var(var_name : String, value : Int32)
    {% begin %}
      case var_name
    {% for ivar in @type.instance_vars %}
      when {{ ivar.name.stringify }}
        @{{ ivar.id }} = value
    {% end %}
      else
        raise "Undefined property: #{var_name}"
    end
    {% end %}
  end

  def get_var(var_name : String)
    {% begin %}
      case var_name
    {% for ivar in @type.instance_vars %}
      when {{ ivar.name.stringify }}
        @{{ ivar.id }}
    {% end %}
      else
        raise "Undefined property: #{var_name}"
    end
    {% end %}
  end
end

Alternatively, we can set up a setters hash table to store a series of Procs that modify the instance’s structural variables to implement the set_var method.

class Dispatcher
  property foo : Int32? = 5
  property bar : Int32? = 12
  property setters : Hash(String, Proc(Int32, Int32))

  def initialize
    @setters = {} of String => Proc(Int32, Int32)
    {% for ivar in @type.instance_vars.reject { |v| v.name == "setters" } %}
      @setters[{{ ivar.name.stringify }}] = ->(v : Int32) { @{{ ivar.id }} = v }
    {% end %}
  end

  def set_var(prop : String, value : Int32)
    if handler = @setters[prop]?
      handler.call(value)
    else
      raise "Undefined property: #{prop}"
    end
  end

  def get_var(var_name : String)
    {% begin %}
      case var_name
    {% for ivar in @type.instance_vars %}
      when {{ ivar.name.stringify }}
        @{{ ivar.id }}
    {% end %}
      else
        raise "Undefined property: #{var_name}"
    end
    {% end %}
  end
end

obj = Dispatcher.new

var = "foo"
val = 222
p obj.set_var(var, val) # => 222

obj.get_var(var)   # => 222
obj.get_var("bar") # => 12
obj.get_var("setters") # => {"foo" => #<Proc(Int32, Int32):0x7ff6d165c520:closure>, "bar" => #<Proc(Int32, Int32):0x7ff6d165c530:closure>}
2 Likes

I like @Sunrise’s approach — it’s very similar to what I would’ve gone with at first. Generalizing beyond all ivars being the same type complicated things a bit, but I managed to whittle it down:

pp foo = Foo.new(0, "")

{
  "a" => 42,
  "b" => "asdf",
}.each do |k, v|
  foo.set(k, v)
end
pp foo

struct Foo
  def initialize(@a : Int32, @b : String)
  end

  def set(var : String, value : T) forall T
    {% for ivar in @type.instance_vars %}
      if var == "{{ivar.name}}" 
        if value.is_a?({{ivar.type}})
          @{{ivar}} = value
          return self
        else
          raise ArgumentError.new("The value provided for #{var.inspect} (#{value.inspect}) must be a {{ivar.type}}")
        end
      end
    {% end %}

    raise ArgumentError.new("Unexpected property name: #{var.inspect}")
  end
end

Usual caveats about mutable structs apply here (mutating a struct you received as a method argument may not mutate the original).

I’ve done something similar long time ago: