Bladeren bron

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
Joe Bolinger 6 jaren geleden
bovenliggende
commit
d2daa38986

+ 33 - 3
ruby/ext/google/protobuf_c/storage.c

@@ -178,9 +178,39 @@ void native_slot_set_value_and_case(const char* name,
       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;

+ 7 - 0
ruby/tests/basic_test.proto

@@ -2,6 +2,8 @@ syntax = "proto3";
 
 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 Outer {
 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;

+ 7 - 0
ruby/tests/basic_test_proto2.proto

@@ -2,6 +2,8 @@ syntax = "proto2";
 
 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 OneofMessage {
   }
 }
 
+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;

+ 50 - 0
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 @@ module CommonTests
     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