diff --git a/ruby/.gitignore b/ruby/.gitignore index bd8745dd85..653309818f 100644 --- a/ruby/.gitignore +++ b/ruby/.gitignore @@ -6,3 +6,4 @@ protobuf-jruby.iml target/ pkg/ tmp/ +tests/google/ \ No newline at end of file diff --git a/ruby/Rakefile b/ruby/Rakefile index 140f5e5149..ad70e3107c 100644 --- a/ruby/Rakefile +++ b/ruby/Rakefile @@ -93,6 +93,7 @@ genproto_output << "tests/test_ruby_package.rb" genproto_output << "tests/test_ruby_package_proto2.rb" genproto_output << "tests/basic_test.rb" genproto_output << "tests/basic_test_proto2.rb" +genproto_output << "tests/wrappers.rb" file "tests/generated_code.rb" => "tests/generated_code.proto" do |file_task| sh "../src/protoc --ruby_out=. tests/generated_code.proto" end @@ -125,6 +126,10 @@ file "tests/basic_test_proto2.rb" => "tests/basic_test_proto2.proto" do |file_ta sh "../src/protoc -I../src -I. --ruby_out=. tests/basic_test_proto2.proto" end +file "tests/wrappers.rb" => "../src/google/protobuf/wrappers.proto" do |file_task| + sh "../src/protoc -I../src -I. --ruby_out=tests ../src/google/protobuf/wrappers.proto" +end + task :genproto => genproto_output task :clean do diff --git a/ruby/ext/google/protobuf_c/message.c b/ruby/ext/google/protobuf_c/message.c index 5537b3860c..850ab0eb5d 100644 --- a/ruby/ext/google/protobuf_c/message.c +++ b/ruby/ext/google/protobuf_c/message.c @@ -119,9 +119,37 @@ enum { METHOD_SETTER = 2, METHOD_CLEAR = 3, METHOD_PRESENCE = 4, - METHOD_ENUM_GETTER = 5 + METHOD_ENUM_GETTER = 5, + METHOD_WRAPPER_GETTER = 6, + METHOD_WRAPPER_SETTER = 7 }; +// Check if the field is a well known wrapper type +static bool is_wrapper_type_field(const upb_fielddef* field) { + char* field_type_name = rb_class2name(field_type_class(field)); + + return strcmp(field_type_name, "Google::Protobuf::DoubleValue") == 0 || + strcmp(field_type_name, "Google::Protobuf::FloatValue") == 0 || + strcmp(field_type_name, "Google::Protobuf::Int32Value") == 0 || + strcmp(field_type_name, "Google::Protobuf::Int64Value") == 0 || + strcmp(field_type_name, "Google::Protobuf::UInt32Value") == 0 || + strcmp(field_type_name, "Google::Protobuf::UInt64Value") == 0 || + strcmp(field_type_name, "Google::Protobuf::BoolValue") == 0 || + strcmp(field_type_name, "Google::Protobuf::StringValue") == 0 || + strcmp(field_type_name, "Google::Protobuf::BytesValue") == 0; +} + +// Get a new Ruby wrapper type and set the initial value +static VALUE ruby_wrapper_type(const upb_fielddef* field, const VALUE* value) { + if (is_wrapper_type_field(field) && value != Qnil) { + VALUE hash = rb_hash_new(); + rb_hash_aset(hash, rb_str_new2("value"), value); + VALUE args[1] = { hash }; + return rb_class_new_instance(1, args, field_type_class(field)); + } + return Qnil; +} + static int extract_method_call(VALUE method_name, MessageHeader* self, const upb_fielddef **f, const upb_oneofdef **o) { Check_Type(method_name, T_SYMBOL); @@ -157,6 +185,34 @@ static int extract_method_call(VALUE method_name, MessageHeader* self, bool has_field = upb_msgdef_lookupname(self->descriptor->msgdef, name, name_len, &test_f, &test_o); + // Look for wrapper type accessor of the form _as_value + if (!has_field && + (accessor_type == METHOD_GETTER || accessor_type == METHOD_SETTER) && + name_len > 9 && strncmp(name + name_len - 9, "_as_value", 9) == 0) { + // Find the field name + char wrapper_field_name[name_len - 8]; + strncpy(wrapper_field_name, name, name_len - 9); + wrapper_field_name[name_len - 7] = '\0'; + + // Check if field exists and is a wrapper type + const upb_oneofdef* test_o_wrapper; + const upb_fielddef* test_f_wrapper; + if (upb_msgdef_lookupname(self->descriptor->msgdef, wrapper_field_name, name_len - 9, + &test_f_wrapper, &test_o_wrapper) && + upb_fielddef_type(test_f_wrapper) == UPB_TYPE_MESSAGE && + is_wrapper_type_field(test_f_wrapper)) { + // It does exist! + has_field = true; + if (accessor_type == METHOD_SETTER) { + accessor_type = METHOD_WRAPPER_SETTER; + } else { + accessor_type = METHOD_WRAPPER_GETTER; + } + test_o = test_o_wrapper; + test_f = test_f_wrapper; + } + } + // Look for enum accessor of the form _const if (!has_field && accessor_type == METHOD_GETTER && name_len > 6 && strncmp(name + name_len - 6, "_const", 6) == 0) { @@ -238,7 +294,7 @@ VALUE Message_method_missing(int argc, VALUE* argv, VALUE _self) { int accessor_type = extract_method_call(argv[0], self, &f, &o); if (accessor_type == METHOD_UNKNOWN || (o == NULL && f == NULL) ) { return rb_call_super(argc, argv); - } else if (accessor_type == METHOD_SETTER) { + } else if (accessor_type == METHOD_SETTER || accessor_type == METHOD_WRAPPER_SETTER) { if (argc != 2) { rb_raise(rb_eArgError, "Expected 2 arguments, received %d", argc); } @@ -275,6 +331,16 @@ VALUE Message_method_missing(int argc, VALUE* argv, VALUE _self) { return Qnil; } else if (accessor_type == METHOD_PRESENCE) { return layout_has(self->descriptor->layout, Message_data(self), f); + } else if (accessor_type == METHOD_WRAPPER_GETTER) { + VALUE value = layout_get(self->descriptor->layout, Message_data(self), f); + if (value != Qnil) { + value = rb_funcall(value, rb_intern("value"), 0); + } + return value; + } else if (accessor_type == METHOD_WRAPPER_SETTER) { + VALUE wrapper = ruby_wrapper_type(f, argv[1]); + layout_set(self->descriptor->layout, Message_data(self), f, wrapper); + return Qnil; } else if (accessor_type == METHOD_ENUM_GETTER) { VALUE enum_type = field_type_class(f); VALUE method = rb_intern("const_get"); diff --git a/ruby/tests/basic_test.proto b/ruby/tests/basic_test.proto index 045714b9ab..3a5a53284c 100644 --- a/ruby/tests/basic_test.proto +++ b/ruby/tests/basic_test.proto @@ -2,6 +2,7 @@ syntax = "proto3"; package basic_test; +import "google/protobuf/wrappers.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; @@ -112,6 +113,22 @@ message Outer { message Inner { } +message Wrapper { + google.protobuf.DoubleValue double = 1; + google.protobuf.FloatValue float = 2; + google.protobuf.Int32Value int32 = 3; + google.protobuf.Int64Value int64 = 4; + google.protobuf.UInt32Value uint32 = 5; + google.protobuf.UInt64Value uint64 = 6; + google.protobuf.BoolValue bool = 7; + google.protobuf.StringValue string = 8; + google.protobuf.BytesValue bytes = 9; + string real_string = 100; + oneof a_oneof { + string oneof_string = 10; + } +} + message TimeMessage { google.protobuf.Timestamp timestamp = 1; google.protobuf.Duration duration = 2; diff --git a/ruby/tests/basic_test_proto2.proto b/ruby/tests/basic_test_proto2.proto index d3a37954ee..e54ed3185a 100644 --- a/ruby/tests/basic_test_proto2.proto +++ b/ruby/tests/basic_test_proto2.proto @@ -2,6 +2,7 @@ syntax = "proto2"; package basic_test_proto2; +import "google/protobuf/wrappers.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; @@ -120,6 +121,22 @@ message OneofMessage { } } +message Wrapper { + optional google.protobuf.DoubleValue double = 1; + optional google.protobuf.FloatValue float = 2; + optional google.protobuf.Int32Value int32 = 3; + optional google.protobuf.Int64Value int64 = 4; + optional google.protobuf.UInt32Value uint32 = 5; + optional google.protobuf.UInt64Value uint64 = 6; + optional google.protobuf.BoolValue bool = 7; + optional google.protobuf.StringValue string = 8; + optional google.protobuf.BytesValue bytes = 9; + optional string real_string = 100; + oneof a_oneof { + string oneof_string = 10; + } +} + message TimeMessage { optional google.protobuf.Timestamp timestamp = 1; optional google.protobuf.Duration duration = 2; diff --git a/ruby/tests/common_tests.rb b/ruby/tests/common_tests.rb index 300c816dc8..2945003b03 100644 --- a/ruby/tests/common_tests.rb +++ b/ruby/tests/common_tests.rb @@ -1,3 +1,5 @@ +require 'google/protobuf/wrappers_pb.rb' + # Defines tests which are common between proto2 and proto3 syntax. # # Requires that the proto messages are exactly the same in proto2 and proto3 syntax @@ -1267,6 +1269,185 @@ module CommonTests assert proto_module::TestMessage.new != nil end + def test_wrapper_getters + m = proto_module::Wrapper.new( + double: Google::Protobuf::DoubleValue.new(value: 2.0), + float: Google::Protobuf::FloatValue.new(value: 4.0), + int32: Google::Protobuf::Int32Value.new(value: 3), + int64: Google::Protobuf::Int64Value.new(value: 4), + uint32: Google::Protobuf::UInt32Value.new(value: 5), + uint64: Google::Protobuf::UInt64Value.new(value: 6), + bool: Google::Protobuf::BoolValue.new(value: true), + string: Google::Protobuf::StringValue.new(value: 'str'), + bytes: Google::Protobuf::BytesValue.new(value: 'fun'), + real_string: '100' + ) + + assert_equal 2.0, m.double_as_value + assert_equal 2.0, m.double.value + assert_equal 4.0, m.float_as_value + assert_equal 4.0, m.float.value + assert_equal 3, m.int32_as_value + assert_equal 3, m.int32.value + assert_equal 4, m.int64_as_value + assert_equal 4, m.int64.value + assert_equal 5, m.uint32_as_value + assert_equal 5, m.uint32.value + assert_equal 6, m.uint64_as_value + assert_equal 6, m.uint64.value + assert_equal true, m.bool_as_value + assert_equal true, m.bool.value + assert_equal 'str', m.string_as_value + assert_equal 'str', m.string.value + assert_equal 'fun', m.bytes_as_value + assert_equal 'fun', m.bytes.value + end + + def test_wrapper_setters_as_value + m = proto_module::Wrapper.new + + m.double_as_value = 4.8 + assert_equal 4.8, m.double_as_value + assert_equal Google::Protobuf::DoubleValue.new(value: 4.8), m.double + m.float_as_value = 2.4 + assert_in_delta 2.4, m.float_as_value + assert_in_delta Google::Protobuf::FloatValue.new(value: 2.4).value, m.float.value + m.int32_as_value = 5 + assert_equal 5, m.int32_as_value + assert_equal Google::Protobuf::Int32Value.new(value: 5), m.int32 + m.int64_as_value = 15 + assert_equal 15, m.int64_as_value + assert_equal Google::Protobuf::Int64Value.new(value: 15), m.int64 + m.uint32_as_value = 50 + assert_equal 50, m.uint32_as_value + assert_equal Google::Protobuf::UInt32Value.new(value: 50), m.uint32 + m.uint64_as_value = 500 + assert_equal 500, m.uint64_as_value + assert_equal Google::Protobuf::UInt64Value.new(value: 500), m.uint64 + m.bool_as_value = false + assert_equal false, m.bool_as_value + assert_equal Google::Protobuf::BoolValue.new(value: false), m.bool + m.string_as_value = 'xy' + assert_equal 'xy', m.string_as_value + assert_equal Google::Protobuf::StringValue.new(value: 'xy'), m.string + m.bytes_as_value = '123' + assert_equal '123', m.bytes_as_value + assert_equal Google::Protobuf::BytesValue.new(value: '123'), m.bytes + + m.double_as_value = nil + assert_nil m.double + assert_nil m.double_as_value + m.float_as_value = nil + assert_nil m.float + assert_nil m.float_as_value + m.int32_as_value = nil + assert_nil m.int32 + assert_nil m.int32_as_value + m.int64_as_value = nil + assert_nil m.int64 + assert_nil m.int64_as_value + m.uint32_as_value = nil + assert_nil m.uint32 + assert_nil m.uint32_as_value + m.uint64_as_value = nil + assert_nil m.uint64 + assert_nil m.uint64_as_value + m.bool_as_value = nil + assert_nil m.bool + assert_nil m.bool_as_value + m.string_as_value = nil + assert_nil m.string + assert_nil m.string_as_value + m.bytes_as_value = nil + assert_nil m.bytes + assert_nil m.bytes_as_value + end + + def test_wrapper_setters + m = proto_module::Wrapper.new + + m.double = Google::Protobuf::DoubleValue.new(value: 4.8) + assert_equal 4.8, m.double_as_value + assert_equal Google::Protobuf::DoubleValue.new(value: 4.8), m.double + m.float = Google::Protobuf::FloatValue.new(value: 2.4) + assert_in_delta 2.4, m.float_as_value + assert_in_delta Google::Protobuf::FloatValue.new(value: 2.4).value, m.float.value + m.int32 = Google::Protobuf::Int32Value.new(value: 5) + assert_equal 5, m.int32_as_value + assert_equal Google::Protobuf::Int32Value.new(value: 5), m.int32 + m.int64 = Google::Protobuf::Int64Value.new(value: 15) + assert_equal 15, m.int64_as_value + assert_equal Google::Protobuf::Int64Value.new(value: 15), m.int64 + m.uint32 = Google::Protobuf::UInt32Value.new(value: 50) + assert_equal 50, m.uint32_as_value + assert_equal Google::Protobuf::UInt32Value.new(value: 50), m.uint32 + m.uint64 = Google::Protobuf::UInt64Value.new(value: 500) + assert_equal 500, m.uint64_as_value + assert_equal Google::Protobuf::UInt64Value.new(value: 500), m.uint64 + m.bool = Google::Protobuf::BoolValue.new(value: false) + assert_equal false, m.bool_as_value + assert_equal Google::Protobuf::BoolValue.new(value: false), m.bool + m.string = Google::Protobuf::StringValue.new(value: 'xy') + assert_equal 'xy', m.string_as_value + assert_equal Google::Protobuf::StringValue.new(value: 'xy'), m.string + m.bytes = Google::Protobuf::BytesValue.new(value: '123') + assert_equal '123', m.bytes_as_value + assert_equal Google::Protobuf::BytesValue.new(value: '123'), m.bytes + + m.double = nil + assert_nil m.double + assert_nil m.double_as_value + m.float = nil + assert_nil m.float + assert_nil m.float_as_value + m.int32 = nil + assert_nil m.int32 + assert_nil m.int32_as_value + m.int64 = nil + assert_nil m.int64 + assert_nil m.int64_as_value + m.uint32 = nil + assert_nil m.uint32 + assert_nil m.uint32_as_value + m.uint64 = nil + assert_nil m.uint64 + assert_nil m.uint64_as_value + m.bool = nil + assert_nil m.bool + assert_nil m.bool_as_value + m.string = nil + assert_nil m.string + assert_nil m.string_as_value + m.bytes = nil + assert_nil m.bytes + assert_nil m.bytes_as_value + end + + def test_wrappers_only + m = proto_module::Wrapper.new(real_string: 'hi', oneof_string: 'there') + + assert_raise(NoMethodError) { m.real_string_as_value } + assert_raise(NoMethodError) { m.as_value } + assert_raise(NoMethodError) { m._as_value } + assert_raise(NoMethodError) { m.oneof_string_as_value } + + m = proto_module::Wrapper.new + m.string_as_value = 'you' + assert_equal 'you', m.string.value + assert_equal 'you', m.string_as_value + assert_raise(NoMethodError) { m.string_ } + assert_raise(NoMethodError) { m.string_X } + assert_raise(NoMethodError) { m.string_XX } + assert_raise(NoMethodError) { m.string_XXX } + assert_raise(NoMethodError) { m.string_XXXX } + assert_raise(NoMethodError) { m.string_XXXXX } + assert_raise(NoMethodError) { m.string_XXXXXX } + assert_raise(NoMethodError) { m.string_XXXXXXX } + assert_raise(NoMethodError) { m.string_XXXXXXXX } + assert_raise(NoMethodError) { m.string_XXXXXXXXX } + assert_raise(NoMethodError) { m.string_XXXXXXXXXX } + end + def test_converts_time m = proto_module::TimeMessage.new