ruby-changes:49022
From: k0kubun <ko1@a...>
Date: Tue, 12 Dec 2017 17:12:48 +0900 (JST)
Subject: [ruby-changes:49022] k0kubun:r61137 (trunk): struct.c: add keyword_init option to Struct.new
k0kubun 2017-12-12 17:12:43 +0900 (Tue, 12 Dec 2017) New Revision: 61137 https://svn.ruby-lang.org/cgi-bin/viewvc.cgi?view=revision&revision=61137 Log: struct.c: add keyword_init option to Struct.new to initialize struct with keyword arguments. [Feature #11925] [close GH-1771] Modified files: trunk/spec/ruby/core/struct/new_spec.rb trunk/struct.c trunk/test/ruby/test_struct.rb Index: struct.c =================================================================== --- struct.c (revision 61136) +++ struct.c (revision 61137) @@ -23,7 +23,7 @@ const rb_iseq_t *rb_method_for_self_aref https://github.com/ruby/ruby/blob/trunk/struct.c#L23 const rb_iseq_t *rb_method_for_self_aset(VALUE name, VALUE arg, rb_insn_func_t func); VALUE rb_cStruct; -static ID id_members, id_back_members; +static ID id_members, id_back_members, id_keyword_init; static VALUE struct_alloc(VALUE); @@ -437,6 +437,7 @@ rb_struct_define_under(VALUE outer, cons https://github.com/ruby/ruby/blob/trunk/struct.c#L437 /* * call-seq: * Struct.new([class_name] [, member_name]+) -> StructClass + * Struct.new([class_name] [, member_name]+, keyword_init: true) -> StructClass * Struct.new([class_name] [, member_name]+) {|StructClass| block } -> StructClass * StructClass.new(value, ...) -> object * StructClass[value, ...] -> object @@ -463,6 +464,13 @@ rb_struct_define_under(VALUE outer, cons https://github.com/ruby/ruby/blob/trunk/struct.c#L464 * Customer.new("Dave", "123 Main") * #=> #<struct Customer name="Dave", address="123 Main"> * + * If keyword_init: true option is given, .new takes Hash instead of Array. + * + * Customer = Struct.new(:name, :address, keyword_init: true) + * #=> Customer + * Customer.new(name: "Dave", address: "123 Main") + * #=> #<struct Customer name="Dave", address="123 Main"> + * * If a block is given it will be evaluated in the context of * +StructClass+, passing the created class as a parameter: * @@ -492,7 +500,7 @@ rb_struct_define_under(VALUE outer, cons https://github.com/ruby/ruby/blob/trunk/struct.c#L500 static VALUE rb_struct_s_def(int argc, VALUE *argv, VALUE klass) { - VALUE name, rest; + VALUE name, rest, keyword_init; long i; VALUE st; st_table *tbl; @@ -506,6 +514,21 @@ rb_struct_s_def(int argc, VALUE *argv, V https://github.com/ruby/ruby/blob/trunk/struct.c#L514 --argc; ++argv; } + + if (RB_TYPE_P(argv[argc-1], T_HASH)) { + VALUE kwargs[1]; + static ID keyword_ids[1]; + + if (!keyword_ids[0]) { + keyword_ids[0] = rb_intern("keyword_init"); + } + rb_get_kwargs(argv[argc-1], keyword_ids, 0, 1, kwargs); + --argc; + keyword_init = kwargs[0]; + } else { + keyword_init = Qfalse; + } + rest = rb_ident_hash_new(); RBASIC_CLEAR_CLASS(rest); tbl = RHASH_TBL(rest); @@ -526,6 +549,7 @@ rb_struct_s_def(int argc, VALUE *argv, V https://github.com/ruby/ruby/blob/trunk/struct.c#L549 st = new_struct(name, klass); } setup_struct(st, rest); + rb_ivar_set(st, id_keyword_init, keyword_init); if (rb_block_given_p()) { rb_mod_module_eval(0, 0, st); } @@ -547,6 +571,30 @@ num_members(VALUE klass) https://github.com/ruby/ruby/blob/trunk/struct.c#L571 /* */ +struct struct_hash_set_arg { + VALUE self; + VALUE unknown_keywords; +}; + +static int rb_struct_pos(VALUE s, VALUE *name); + +static int +struct_hash_set_i(VALUE key, VALUE val, VALUE arg) +{ + struct struct_hash_set_arg *args = (struct struct_hash_set_arg *)arg; + int i = rb_struct_pos(args->self, &key); + if (i < 0) { + if (args->unknown_keywords == Qnil) { + args->unknown_keywords = rb_ary_new(); + } + rb_ary_push(args->unknown_keywords, key); + } else { + rb_struct_modify(args->self); + RSTRUCT_SET(args->self, i, val); + } + return ST_CONTINUE; +} + static VALUE rb_struct_initialize_m(int argc, const VALUE *argv, VALUE self) { @@ -555,14 +603,29 @@ rb_struct_initialize_m(int argc, const V https://github.com/ruby/ruby/blob/trunk/struct.c#L603 rb_struct_modify(self); n = num_members(klass); - if (n < argc) { - rb_raise(rb_eArgError, "struct size differs"); - } - for (i=0; i<argc; i++) { - RSTRUCT_SET(self, i, argv[i]); - } - if (n > argc) { - rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self)+argc, n-argc); + if (argc > 0 && RTEST(struct_ivar_get(klass, id_keyword_init))) { + struct struct_hash_set_arg arg; + if (argc > 2 || !RB_TYPE_P(argv[0], T_HASH)) { + rb_raise(rb_eArgError, "wrong number of arguments (given %d, expected 0)", argc); + } + rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self), n); + arg.self = self; + arg.unknown_keywords = Qnil; + rb_hash_foreach(argv[0], struct_hash_set_i, (VALUE)&arg); + if (arg.unknown_keywords != Qnil) { + rb_raise(rb_eArgError, "unknown keywords: %s", + RSTRING_PTR(rb_ary_join(arg.unknown_keywords, rb_str_new2(", ")))); + } + } else { + if (n < argc) { + rb_raise(rb_eArgError, "struct size differs"); + } + for (i=0; i<argc; i++) { + RSTRUCT_SET(self, i, argv[i]); + } + if (n > argc) { + rb_mem_clear((VALUE *)RSTRUCT_CONST_PTR(self)+argc, n-argc); + } } return Qnil; } @@ -1226,6 +1289,7 @@ Init_Struct(void) https://github.com/ruby/ruby/blob/trunk/struct.c#L1289 { id_members = rb_intern("__members__"); id_back_members = rb_intern("__members_back__"); + id_keyword_init = rb_intern("__keyword_init__"); InitVM(Struct); } Index: test/ruby/test_struct.rb =================================================================== --- test/ruby/test_struct.rb (revision 61136) +++ test/ruby/test_struct.rb (revision 61137) @@ -92,6 +92,23 @@ module TestStruct https://github.com/ruby/ruby/blob/trunk/test/ruby/test_struct.rb#L92 assert_equal([:utime, :stime, :cutime, :cstime], Process.times.members) end + def test_struct_new_with_keyword_init + @Struct.new("KeywordArgsTrue", :a, :b, keyword_init: true) + @Struct.new("KeywordArgsFalse", :a, :b, keyword_init: false) + + assert_raise(ArgumentError) { @Struct::KeywordArgsTrue.new(1, 2) } + assert_nothing_raised { @Struct::KeywordArgsFalse.new(1, 2) } + assert_nothing_raised { @Struct::KeywordArgsTrue.new(a: 1, b: 2) } + assert_raise(ArgumentError) { @Struct::KeywordArgsTrue.new(1, b: 2) } + assert_raise(ArgumentError) { @Struct::KeywordArgsTrue.new(a: 1, b: 2, c: 3) } + assert_equal @Struct::KeywordArgsTrue.new(a: 1, b: 2).values, @Struct::KeywordArgsFalse.new(1, 2).values + + @Struct.instance_eval do + remove_const(:KeywordArgsTrue) + remove_const(:KeywordArgsFalse) + end + end + def test_initialize klass = @Struct.new(:a) assert_raise(ArgumentError) { klass.new(1, 2) } Index: spec/ruby/core/struct/new_spec.rb =================================================================== --- spec/ruby/core/struct/new_spec.rb (revision 61136) +++ spec/ruby/core/struct/new_spec.rb (revision 61137) @@ -60,7 +60,7 @@ describe "Struct.new" do https://github.com/ruby/ruby/blob/trunk/spec/ruby/core/struct/new_spec.rb#L60 lambda { Struct.new(:animal, nil) }.should raise_error(TypeError) lambda { Struct.new(:animal, true) }.should raise_error(TypeError) lambda { Struct.new(:animal, ['chris', 'evan']) }.should raise_error(TypeError) - lambda { Struct.new(:animal, { name: 'chris' }) }.should raise_error(TypeError) + lambda { Struct.new(:animal, { name: 'chris' }) }.should raise_error(ArgumentError) end it "raises a TypeError if object is not a Symbol" do -- ML: ruby-changes@q... Info: http://www.atdot.net/~ko1/quickml/