From 512d25607bcaaffd9232783d309fa768c66e6066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20=C5=9Awi=C4=85tkowski?= Date: Mon, 14 Jul 2025 15:06:12 +0200 Subject: [PATCH] Support :keyword_init argument when defining a new struct --- mrbgems/mruby-struct/src/struct.c | 44 ++++++++++++++++++++---- mrbgems/mruby-struct/test/struct.rb | 52 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/mrbgems/mruby-struct/src/struct.c b/mrbgems/mruby-struct/src/struct.c index 9b78a5169..bc14dcd86 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)); } @@ -335,6 +351,7 @@ mrb_struct_initialize_withKw(mrb_state *mrb, mrb_value hash, mrb_value self) } } + /* Check if all keys in the hash are valid members */ mrb_value keys = mrb_hash_keys(mrb, hash); mrb_value invalid_keys = mrb_ary_new(mrb); @@ -350,8 +367,7 @@ mrb_struct_initialize_withKw(mrb_state *mrb, mrb_value hash, mrb_value self) /* 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 keyword%s: %S", - RARRAY_LEN(invalid_keys) > 1 ? "s" : "", keys_str); + mrb_raisef(mrb, E_ARGUMENT_ERROR, "unknown keywords: %S", keys_str); } return self; @@ -362,15 +378,31 @@ 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); - /* If we get a single hash argument, treat it as keyword arguments */ - if (argc == 1 && mrb_hash_p(argv[0])) { - return mrb_struct_initialize_withKw(mrb, argv[0], self); - } else { + 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 da41d04cb..a2fbcdc3d 100644 --- a/mrbgems/mruby-struct/test/struct.rb +++ b/mrbgems/mruby-struct/test/struct.rb @@ -245,7 +245,59 @@ assert "Struct initialize with keyword arguments" do 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