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