Add native type setters for Timestamp and Duration in Ruby (#5751) * add implicit time conversion * add duration * add init test * more tests * add type check and alternative c type check * add rational and bigdecimal * use rb_obj_is_kind_of * use native time check * chain implicit conversions * remove unused variable
diff --git a/ruby/ext/google/protobuf_c/storage.c b/ruby/ext/google/protobuf_c/storage.c index e9fea23..ba4f831 100644 --- a/ruby/ext/google/protobuf_c/storage.c +++ b/ruby/ext/google/protobuf_c/storage.c
@@ -178,9 +178,39 @@ if (CLASS_OF(value) == CLASS_OF(Qnil)) { value = Qnil; } else if (CLASS_OF(value) != type_class) { - rb_raise(cTypeError, - "Invalid type %s to assign to submessage field '%s'.", - rb_class2name(CLASS_OF(value)), name); + // check for possible implicit conversions + VALUE converted_value = NULL; + char* field_type_name = rb_class2name(type_class); + + if (strcmp(field_type_name, "Google::Protobuf::Timestamp") == 0 && + rb_obj_is_kind_of(value, rb_cTime)) { + // Time -> Google::Protobuf::Timestamp + VALUE hash = rb_hash_new(); + rb_hash_aset(hash, rb_str_new2("seconds"), rb_funcall(value, rb_intern("to_i"), 0)); + rb_hash_aset(hash, rb_str_new2("nanos"), rb_funcall(value, rb_intern("nsec"), 0)); + VALUE args[1] = { hash }; + converted_value = rb_class_new_instance(1, args, type_class); + } else if (strcmp(field_type_name, "Google::Protobuf::Duration") == 0 && + rb_obj_is_kind_of(value, rb_cNumeric)) { + // Numeric -> Google::Protobuf::Duration + VALUE hash = rb_hash_new(); + rb_hash_aset(hash, rb_str_new2("seconds"), rb_funcall(value, rb_intern("to_i"), 0)); + VALUE n_value = rb_funcall(value, rb_intern("remainder"), 1, INT2NUM(1)); + n_value = rb_funcall(n_value, rb_intern("*"), 1, INT2NUM(1000000000)); + n_value = rb_funcall(n_value, rb_intern("round"), 0); + rb_hash_aset(hash, rb_str_new2("nanos"), n_value); + VALUE args[1] = { hash }; + converted_value = rb_class_new_instance(1, args, type_class); + } + + // raise if no suitable conversaion could be found + if (converted_value == NULL) { + rb_raise(cTypeError, + "Invalid type %s to assign to submessage field '%s'.", + rb_class2name(CLASS_OF(value)), name); + } else { + value = converted_value; + } } DEREF(memory, VALUE) = value; break;
diff --git a/ruby/tests/basic_test.proto b/ruby/tests/basic_test.proto index 0ef6066..045714b 100644 --- a/ruby/tests/basic_test.proto +++ b/ruby/tests/basic_test.proto
@@ -2,6 +2,8 @@ package basic_test; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; message Foo { @@ -110,6 +112,11 @@ message Inner { } +message TimeMessage { + google.protobuf.Timestamp timestamp = 1; + google.protobuf.Duration duration = 2; +} + message Enumer { TestEnum optional_enum = 1; repeated TestEnum repeated_enum = 2;
diff --git a/ruby/tests/basic_test_proto2.proto b/ruby/tests/basic_test_proto2.proto index 4641a6f..d3a3795 100644 --- a/ruby/tests/basic_test_proto2.proto +++ b/ruby/tests/basic_test_proto2.proto
@@ -2,6 +2,8 @@ package basic_test_proto2; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; message Foo { @@ -118,6 +120,11 @@ } } +message TimeMessage { + optional google.protobuf.Timestamp timestamp = 1; + optional google.protobuf.Duration duration = 2; +} + message Enumer { optional TestEnum optional_enum = 11; repeated TestEnum repeated_enum = 22;
diff --git a/ruby/tests/common_tests.rb b/ruby/tests/common_tests.rb index 170d8da..300c816 100644 --- a/ruby/tests/common_tests.rb +++ b/ruby/tests/common_tests.rb
@@ -3,6 +3,9 @@ # Requires that the proto messages are exactly the same in proto2 and proto3 syntax # and that the including class should define a 'proto_module' method which returns # the enclosing module of the proto message classes. + +require 'bigdecimal' + module CommonTests # Ruby 2.5 changed to raise FrozenError instead of RuntimeError FrozenErrorType = Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.5') ? RuntimeError : FrozenError @@ -1264,6 +1267,53 @@ assert proto_module::TestMessage.new != nil end + def test_converts_time + m = proto_module::TimeMessage.new + + m.timestamp = Google::Protobuf::Timestamp.new(seconds: 5, nanos: 6) + assert_kind_of Google::Protobuf::Timestamp, m.timestamp + assert_equal 5, m.timestamp.seconds + assert_equal 6, m.timestamp.nanos + + m.timestamp = Time.at(9466, 123456.789) + assert_equal Google::Protobuf::Timestamp.new(seconds: 9466, nanos: 123456789), m.timestamp + + m = proto_module::TimeMessage.new(timestamp: Time.at(1)) + assert_equal Google::Protobuf::Timestamp.new(seconds: 1, nanos: 0), m.timestamp + + assert_raise(Google::Protobuf::TypeError) { m.timestamp = 2 } + assert_raise(Google::Protobuf::TypeError) { m.timestamp = 2.4 } + assert_raise(Google::Protobuf::TypeError) { m.timestamp = '4' } + assert_raise(Google::Protobuf::TypeError) { m.timestamp = proto_module::TimeMessage.new } + end + + def test_converts_duration + m = proto_module::TimeMessage.new + + m.duration = Google::Protobuf::Duration.new(seconds: 2, nanos: 22) + assert_kind_of Google::Protobuf::Duration, m.duration + assert_equal 2, m.duration.seconds + assert_equal 22, m.duration.nanos + + m.duration = 10.5 + assert_equal Google::Protobuf::Duration.new(seconds: 10, nanos: 500_000_000), m.duration + + m.duration = 200 + assert_equal Google::Protobuf::Duration.new(seconds: 200, nanos: 0), m.duration + + m.duration = Rational(3, 2) + assert_equal Google::Protobuf::Duration.new(seconds: 1, nanos: 500_000_000), m.duration + + m.duration = BigDecimal.new("5") + assert_equal Google::Protobuf::Duration.new(seconds: 5, nanos: 0), m.duration + + m = proto_module::TimeMessage.new(duration: 1.1) + assert_equal Google::Protobuf::Duration.new(seconds: 1, nanos: 100_000_000), m.duration + + assert_raise(Google::Protobuf::TypeError) { m.duration = '2' } + assert_raise(Google::Protobuf::TypeError) { m.duration = proto_module::TimeMessage.new } + end + def test_freeze m = proto_module::TestMessage.new m.optional_int32 = 10