diff --git a/mrbgems/mruby-struct/src/struct.c b/mrbgems/mruby-struct/src/struct.c index 4c4a23272..3f7c90799 100644 --- a/mrbgems/mruby-struct/src/struct.c +++ b/mrbgems/mruby-struct/src/struct.c @@ -258,12 +258,24 @@ mrb_struct_s_def(mrb_state *mrb, mrb_value klass) mrb_value b; const mrb_value *argv; mrb_int argc; + mrb_value keyword_init_val = mrb_nil_value(); mrb_get_args(mrb, "*&", &argv, &argc, &b); if (argc == 0) { mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong number of arguments (given 0, expected 1+)"); } + /* Check for keyword_init option in arguments */ + if (argc > 0 && mrb_hash_p(argv[argc-1])) { + mrb_value options = argv[argc-1]; + mrb_sym keyword_init_sym = mrb_intern_lit(mrb, "keyword_init"); + + if (mrb_hash_key_p(mrb, options, mrb_symbol_value(keyword_init_sym))) { + keyword_init_val = mrb_hash_get(mrb, options, mrb_symbol_value(keyword_init_sym)); + argc--; /* Don't treat the options hash as a member name */ + } + } + const mrb_value *pargv = argv; mrb_int argcnt = argc; if (argc > 0 && !mrb_symbol_p(argv[0])) { @@ -290,6 +302,10 @@ mrb_struct_s_def(mrb_state *mrb, mrb_value klass) } mrb_value st = make_struct(mrb, name, members, mrb_class_ptr(klass)); + + /* Set keyword_init value on the struct class */ + mrb_iv_set(mrb, st, mrb_intern_lit(mrb, "@__keyword_init__"), keyword_init_val); + if (!mrb_nil_p(b)) { mrb_yield_with_class(mrb, b, 1, &st, st, mrb_class_ptr(st)); } @@ -317,14 +333,79 @@ mrb_struct_initialize_withArg(mrb_state *mrb, mrb_int argc, const mrb_value *arg return self; } +static mrb_value +mrb_struct_initialize_withKw(mrb_state *mrb, mrb_value hash, mrb_value self) +{ + mrb_value members = struct_members(mrb, self); + mrb_int member_count = num_members(mrb, self); + + /* Initialize members from hash, defaulting to nil */ + for (mrb_int i = 0; i < member_count; i++) { + mrb_value member = RARRAY_PTR(members)[i]; + if (mrb_hash_key_p(mrb, hash, member)) { + mrb_value val = mrb_hash_get(mrb, hash, member); + mrb_ary_set(mrb, self, i, val); + } + else { + mrb_ary_set(mrb, self, i, mrb_nil_value()); + } + } + + /* Create a hash of valid members for faster lookup */ + mrb_value members_hash = mrb_hash_new(mrb); + for (mrb_int i = 0; i < RARRAY_LEN(members); i++) { + mrb_hash_set(mrb, members_hash, RARRAY_PTR(members)[i], mrb_true_value()); + } + + /* Check if all keys in the hash are valid members */ + mrb_value invalid_keys = mrb_ary_new(mrb); + mrb_value keys = mrb_hash_keys(mrb, hash); + for (mrb_int i = 0; i < RARRAY_LEN(keys); i++) { + mrb_value key = RARRAY_PTR(keys)[i]; + if (!mrb_hash_key_p(mrb, members_hash, key)) { + mrb_ary_push(mrb, invalid_keys, key); + } + } + + /* If there are any invalid keys, raise an error with all of them */ + if (RARRAY_LEN(invalid_keys) > 0) { + mrb_value keys_str = mrb_funcall(mrb, invalid_keys, "join", 1, mrb_str_new_lit(mrb, ", ")); + mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown keywords: %S", keys_str); + } + + return self; +} + static mrb_value mrb_struct_initialize(mrb_state *mrb, mrb_value self) { const mrb_value *argv; mrb_int argc; + mrb_value klass, keyword_init; mrb_get_args(mrb, "*", &argv, &argc); - return mrb_struct_initialize_withArg(mrb, argc, argv, self); + + klass = mrb_obj_value(mrb_obj_class(mrb, self)); + keyword_init = mrb_iv_get(mrb, klass, mrb_intern_lit(mrb, "@__keyword_init__")); + + if (mrb_test(keyword_init)) { /* keyword_init: true or other truthy value */ + if (argc > 1 || (argc == 1 && !mrb_hash_p(argv[0]))) { + mrb_raise(mrb, E_ARGUMENT_ERROR, "wrong arguments, expected keyword arguments"); + } + mrb_value hash = (argc == 1) ? argv[0] : mrb_hash_new(mrb); + return mrb_struct_initialize_withKw(mrb, hash, self); + } + else if (mrb_equal(mrb, keyword_init, mrb_false_value())) { /* keyword_init: false */ + return mrb_struct_initialize_withArg(mrb, argc, argv, self); + } + else { /* keyword_init: nil (default) */ + if (argc == 1 && mrb_hash_p(argv[0])) { + return mrb_struct_initialize_withKw(mrb, argv[0], self); + } + else { + return mrb_struct_initialize_withArg(mrb, argc, argv, self); + } + } } /* 15.2.18.4.9 */ diff --git a/mrbgems/mruby-struct/test/struct.rb b/mrbgems/mruby-struct/test/struct.rb index d10dc866c..deed70680 100644 --- a/mrbgems/mruby-struct/test/struct.rb +++ b/mrbgems/mruby-struct/test/struct.rb @@ -229,3 +229,75 @@ assert 'method visibility with Struct' do c.new.bad! end end + +assert "Struct initialize with keyword arguments" do + c = Struct.new(:foo, :bar) + + o = c.new(foo: 1, bar: 2) + assert_equal 1, o.foo + assert_equal 2, o.bar + + o2 = c.new(bar: 1, foo: 2) + assert_equal 2, o2.foo + assert_equal 1, o2.bar + + o3 = c.new(foo: :test) + assert_equal :test, o3.foo + assert_equal nil, o3.bar + + o4 = c.new + assert_equal nil, o4.foo + assert_equal nil, o4.bar + + assert_raise_with_message_pattern(ArgumentError, "unknown keywords: roo, baq") do + c.new(foo: 1, roo: nil, baq: :test) + end +end + +assert "Struct initialize when :keyword_init is true" do + c = Struct.new(:foo, :bar, keyword_init: true) + + o = c.new(foo: 1, bar: 2) + assert_equal 1, o.foo + assert_equal 2, o.bar + + o2 = c.new + assert_equal nil, o2.foo + assert_equal nil, o2.bar + + assert_raise(ArgumentError) do + c.new(1, 2) + end + + assert_raise(ArgumentError) do + c.new({foo: 1}, {bar: 2}) + end +end + +assert "Struct initialize when :keyword_init is false" do + c = Struct.new(:foo, :bar, keyword_init: false) + + o = c.new(1, 2) + assert_equal 1, o.foo + assert_equal 2, o.bar + + o2 = c.new(foo: 1, bar: 2) + assert_equal({foo: 1, bar: 2}, o2.foo) + assert_equal nil, o2.bar + + o3 = c.new + assert_equal nil, o3.foo + assert_equal nil, o3.bar +end + +assert "Struct initialize when :keyword_init is non-boolean value (treat as true)" do + c = Struct.new(:foo, :bar, keyword_init: 12) + + o = c.new(foo: 1, bar: 2) + assert_equal 1, o.foo + assert_equal 2, o.bar + + assert_raise(ArgumentError) do + c.new(1, 2) + end +end