зеркало из https://github.com/github/ruby.git
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) # => #<data Point x: 3, y: 2> ``` 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 <XrXr@users.noreply.github.com>
This commit is contained in:
Родитель
398aaed2f0
Коммит
99cee85775
59
struct.c
59
struct.c
|
@ -1832,6 +1832,63 @@ rb_data_init_copy(VALUE copy, VALUE s)
|
||||||
return copy;
|
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 # #<data Point x=0, y=0>
|
||||||
|
* p up # #<data Point x=1, y=0>
|
||||||
|
* p right # #<data Point x=0, y=1>
|
||||||
|
* p up_and_right # #<data Point x=1, y=1>
|
||||||
|
*
|
||||||
|
* 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:
|
* call-seq:
|
||||||
* inspect -> string
|
* 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", rb_data_deconstruct, 0);
|
||||||
rb_define_method(rb_cData, "deconstruct_keys", rb_data_deconstruct_keys, 1);
|
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
|
#undef rb_intern
|
||||||
|
|
|
@ -158,6 +158,65 @@ class TestData < Test::Unit::TestCase
|
||||||
assert_not_operator(o1, :eql?, o3)
|
assert_not_operator(o1, :eql?, o3)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_with
|
||||||
|
klass = Data.define(:foo, :bar)
|
||||||
|
source = klass.new(foo: 1, bar: 2)
|
||||||
|
|
||||||
|
# Simple
|
||||||
|
test = source.with
|
||||||
|
assert_equal(source.object_id, test.object_id)
|
||||||
|
|
||||||
|
# Changes
|
||||||
|
test = source.with(foo: 10)
|
||||||
|
|
||||||
|
assert_equal(1, source.foo)
|
||||||
|
assert_equal(2, source.bar)
|
||||||
|
assert_equal(source, klass.new(foo: 1, bar: 2))
|
||||||
|
|
||||||
|
assert_equal(10, test.foo)
|
||||||
|
assert_equal(2, test.bar)
|
||||||
|
assert_equal(test, klass.new(foo: 10, bar: 2))
|
||||||
|
|
||||||
|
test = source.with(foo: 10, bar: 20)
|
||||||
|
|
||||||
|
assert_equal(1, source.foo)
|
||||||
|
assert_equal(2, source.bar)
|
||||||
|
assert_equal(source, klass.new(foo: 1, bar: 2))
|
||||||
|
|
||||||
|
assert_equal(10, test.foo)
|
||||||
|
assert_equal(20, test.bar)
|
||||||
|
assert_equal(test, klass.new(foo: 10, bar: 20))
|
||||||
|
|
||||||
|
# Keyword splat
|
||||||
|
changes = { foo: 10, bar: 20 }
|
||||||
|
test = source.with(**changes)
|
||||||
|
|
||||||
|
assert_equal(1, source.foo)
|
||||||
|
assert_equal(2, source.bar)
|
||||||
|
assert_equal(source, klass.new(foo: 1, bar: 2))
|
||||||
|
|
||||||
|
assert_equal(10, test.foo)
|
||||||
|
assert_equal(20, test.bar)
|
||||||
|
assert_equal(test, klass.new(foo: 10, bar: 20))
|
||||||
|
|
||||||
|
# Wrong protocol
|
||||||
|
assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
|
||||||
|
source.with(10)
|
||||||
|
end
|
||||||
|
assert_raise_with_message(ArgumentError, "unknown keywords: :baz, :quux") do
|
||||||
|
source.with(foo: 1, bar: 2, baz: 3, quux: 4)
|
||||||
|
end
|
||||||
|
assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
|
||||||
|
source.with(1, bar: 2)
|
||||||
|
end
|
||||||
|
assert_raise_with_message(ArgumentError, "wrong number of arguments (given 2, expected 0)") do
|
||||||
|
source.with(1, 2)
|
||||||
|
end
|
||||||
|
assert_raise_with_message(ArgumentError, "wrong number of arguments (given 1, expected 0)") do
|
||||||
|
source.with({ bar: 2 })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_memberless
|
def test_memberless
|
||||||
klass = Data.define
|
klass = Data.define
|
||||||
|
|
||||||
|
|
Загрузка…
Ссылка в новой задаче