From 99cee85775bc5656b942bf4e1b0568a8f40addcd Mon Sep 17 00:00:00 2001 From: Ufuk Kayserilioglu Date: Thu, 22 Dec 2022 02:27:38 +0200 Subject: Add copy with changes functionality for Data objects (#6766) Implements [Feature #19000] This commit adds copy with changes functionality for `Data` objects using a new method `Data#with`. Since Data objects are immutable, the only way to change them is by creating a copy. This PR adds a `with` method for `Data` class instances that optionally takes keyword arguments. If the `with` method is called with no arguments, the behaviour is the same as the `Kernel#dup` method, i.e. a new shallow copy is created with no field values changed. However, if keyword arguments are supplied to the `with` method, then the copy is created with the specified field values changed. For example: ```ruby Point = Data.define(:x, :y) point = Point.new(x: 1, y: 2) point.with(x: 3) # => # ``` Passing positional arguments to `with` or passing keyword arguments to it that do not correspond to any of the members of the Data class will raise an `ArgumentError`. Co-authored-by: Alan Wu --- struct.c | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) (limited to 'struct.c') diff --git a/struct.c b/struct.c index 3733669eab..825987917d 100644 --- a/struct.c +++ b/struct.c @@ -1832,6 +1832,63 @@ rb_data_init_copy(VALUE copy, VALUE s) return copy; } +/* + * call-seq: + * with(**kwargs) -> instance + * + * Returns a shallow copy of +self+ --- the instance variables of + * +self+ are copied, but not the objects they reference. + * + * If the method is supplied any keyword arguments, the copy will + * be created with the respective field values updated to use the + * supplied keyword argument values. Note that it is an error to + * supply a keyword that the Data class does not have as a member. + * + * Point = Data.define(:x, :y) + * + * origin = Point.new(x: 0, y: 0) + * + * up = origin.with(x: 1) + * right = origin.with(y: 1) + * up_and_right = up.with(y: 1) + * + * p origin # # + * p up # # + * p right # # + * p up_and_right # # + * + * out = origin.with(z: 1) # ArgumentError: unknown keyword: :z + * some_point = origin.with(1, 2) # ArgumentError: expected keyword arguments, got positional arguments + * + */ + +static VALUE +rb_data_with(int argc, const VALUE *argv, VALUE self) +{ + VALUE kwargs; + rb_scan_args(argc, argv, "0:", &kwargs); + if (NIL_P(kwargs)) { + return self; + } + + VALUE copy = rb_obj_alloc(rb_obj_class(self)); + rb_struct_init_copy(copy, self); + + struct struct_hash_set_arg arg; + arg.self = copy; + arg.unknown_keywords = Qnil; + rb_hash_foreach(kwargs, struct_hash_set_i, (VALUE)&arg); + // Freeze early before potentially raising, so that we don't leave an + // unfrozen copy on the heap, which could get exposed via ObjectSpace. + RB_OBJ_FREEZE_RAW(copy); + + if (arg.unknown_keywords != Qnil) { + rb_exc_raise(rb_keyword_error_new("unknown", arg.unknown_keywords)); + } + + return copy; +} + /* * call-seq: * inspect -> string @@ -2205,6 +2262,8 @@ InitVM_Struct(void) rb_define_method(rb_cData, "deconstruct", rb_data_deconstruct, 0); rb_define_method(rb_cData, "deconstruct_keys", rb_data_deconstruct_keys, 1); + + rb_define_method(rb_cData, "with", rb_data_with, -1); } #undef rb_intern -- cgit v1.2.3