From d1b52a00e002fd7a4adbcdeafa0634de6088a88f Mon Sep 17 00:00:00 2001 From: Adam Greene Date: Sun, 3 May 2015 10:55:10 -0700 Subject: [PATCH] adding and simplifying encoders/decoders * make consistent between mri and jruby * create a #to_h and have it use symbols for keys * add #to_json and #to_proto helpers on the Google::Protobuf message classes --- ruby/ext/google/protobuf_c/encode_decode.c | 47 -------------- ruby/ext/google/protobuf_c/message.c | 31 ++++++++++ ruby/ext/google/protobuf_c/protobuf.c | 7 --- ruby/ext/google/protobuf_c/protobuf.h | 6 +- ruby/lib/google/protobuf.rb | 25 ++++++++ ruby/lib/google/protobuf/message_exts.rb | 53 ++++++++++++++++ .../google/protobuf/jruby/RubyDescriptor.java | 2 + .../com/google/protobuf/jruby/RubyMap.java | 2 +- .../google/protobuf/jruby/RubyMessage.java | 12 ++-- .../google/protobuf/jruby/RubyProtobuf.java | 50 --------------- ruby/tests/basic.rb | 61 +++++++++++++++++++ 11 files changed, 182 insertions(+), 114 deletions(-) create mode 100644 ruby/lib/google/protobuf/message_exts.rb diff --git a/ruby/ext/google/protobuf_c/encode_decode.c b/ruby/ext/google/protobuf_c/encode_decode.c index 5730504d5707..f9a046cbc15c 100644 --- a/ruby/ext/google/protobuf_c/encode_decode.c +++ b/ruby/ext/google/protobuf_c/encode_decode.c @@ -1118,50 +1118,3 @@ VALUE Message_encode_json(VALUE klass, VALUE msg_rb) { return ret; } -/* - * call-seq: - * Google::Protobuf.encode(msg) => bytes - * - * Encodes the given message object to protocol buffers wire format. This is an - * alternative to the #encode method on msg's class. - */ -VALUE Google_Protobuf_encode(VALUE self, VALUE msg_rb) { - VALUE klass = CLASS_OF(msg_rb); - return Message_encode(klass, msg_rb); -} - -/* - * call-seq: - * Google::Protobuf.encode_json(msg) => json_string - * - * Encodes the given message object to its JSON representation. This is an - * alternative to the #encode_json method on msg's class. - */ -VALUE Google_Protobuf_encode_json(VALUE self, VALUE msg_rb) { - VALUE klass = CLASS_OF(msg_rb); - return Message_encode_json(klass, msg_rb); -} - -/* - * call-seq: - * Google::Protobuf.decode(class, bytes) => msg - * - * Decodes the given bytes as protocol buffers wire format under the - * interpretation given by the given class's message definition. This is an - * alternative to the #decode method on the given class. - */ -VALUE Google_Protobuf_decode(VALUE self, VALUE klass, VALUE msg_rb) { - return Message_decode(klass, msg_rb); -} - -/* - * call-seq: - * Google::Protobuf.decode_json(class, json_string) => msg - * - * Decodes the given JSON string under the interpretation given by the given - * class's message definition. This is an alternative to the #decode_json method - * on the given class. - */ -VALUE Google_Protobuf_decode_json(VALUE self, VALUE klass, VALUE msg_rb) { - return Message_decode_json(klass, msg_rb); -} diff --git a/ruby/ext/google/protobuf_c/message.c b/ruby/ext/google/protobuf_c/message.c index 7e58a6178baf..6e850427d33c 100644 --- a/ruby/ext/google/protobuf_c/message.c +++ b/ruby/ext/google/protobuf_c/message.c @@ -329,6 +329,30 @@ VALUE Message_inspect(VALUE _self) { return str; } + +VALUE Message_to_h(VALUE _self) { + MessageHeader* self; + TypedData_Get_Struct(_self, MessageHeader, &Message_type, self); + + VALUE hash = rb_hash_new(); + + upb_msg_field_iter it; + for (upb_msg_field_begin(&it, self->descriptor->msgdef); + !upb_msg_field_done(&it); + upb_msg_field_next(&it)) { + const upb_fielddef* field = upb_msg_iter_field(&it); + VALUE msg_value = layout_get(self->descriptor->layout, Message_data(self), field); + VALUE msg_key = ID2SYM(rb_intern(upb_fielddef_name(field))); + if (upb_fielddef_label(field) == UPB_LABEL_REPEATED) { + msg_value = RepeatedField_to_ary(msg_value); + } + rb_hash_aset(hash, msg_key, msg_value); + } + return hash; +} + + + /* * call-seq: * Message.[](index) => value @@ -399,6 +423,10 @@ VALUE build_class_from_descriptor(Descriptor* desc) { rb_cObject); rb_iv_set(klass, kDescriptorInstanceVar, get_def_obj(desc->msgdef)); rb_define_alloc_func(klass, Message_alloc); + rb_require("google/protobuf/message_exts"); + rb_include_module(klass, rb_eval_string("Google::Protobuf::MessageExts")); + rb_extend_object(klass, rb_eval_string("Google::Protobuf::MessageExts::ClassMethods")); + rb_define_method(klass, "method_missing", Message_method_missing, -1); rb_define_method(klass, "initialize", Message_initialize, -1); @@ -407,6 +435,8 @@ VALUE build_class_from_descriptor(Descriptor* desc) { rb_define_method(klass, "clone", Message_dup, 0); rb_define_method(klass, "==", Message_eq, 1); rb_define_method(klass, "hash", Message_hash, 0); + rb_define_method(klass, "to_h", Message_to_h, 0); + rb_define_method(klass, "to_hash", Message_to_h, 0); rb_define_method(klass, "inspect", Message_inspect, 0); rb_define_method(klass, "[]", Message_index, 1); rb_define_method(klass, "[]=", Message_index_set, 2); @@ -415,6 +445,7 @@ VALUE build_class_from_descriptor(Descriptor* desc) { rb_define_singleton_method(klass, "decode_json", Message_decode_json, 1); rb_define_singleton_method(klass, "encode_json", Message_encode_json, 1); rb_define_singleton_method(klass, "descriptor", Message_descriptor, 0); + return klass; } diff --git a/ruby/ext/google/protobuf_c/protobuf.c b/ruby/ext/google/protobuf_c/protobuf.c index d2d3503386fa..8ab518a5831f 100644 --- a/ruby/ext/google/protobuf_c/protobuf.c +++ b/ruby/ext/google/protobuf_c/protobuf.c @@ -86,13 +86,6 @@ void Init_protobuf_c() { RepeatedField_register(protobuf); Map_register(protobuf); - rb_define_singleton_method(protobuf, "encode", Google_Protobuf_encode, 1); - rb_define_singleton_method(protobuf, "decode", Google_Protobuf_decode, 2); - rb_define_singleton_method(protobuf, "encode_json", - Google_Protobuf_encode_json, 1); - rb_define_singleton_method(protobuf, "decode_json", - Google_Protobuf_decode_json, 2); - rb_define_singleton_method(protobuf, "deep_copy", Google_Protobuf_deep_copy, 1); diff --git a/ruby/ext/google/protobuf_c/protobuf.h b/ruby/ext/google/protobuf_c/protobuf.h index d8a327aa7d09..ef00b5fa2bd7 100644 --- a/ruby/ext/google/protobuf_c/protobuf.h +++ b/ruby/ext/google/protobuf_c/protobuf.h @@ -374,6 +374,7 @@ VALUE RepeatedField_clear(VALUE _self); VALUE RepeatedField_length(VALUE _self); VALUE RepeatedField_dup(VALUE _self); VALUE RepeatedField_deep_copy(VALUE _self); +VALUE RepeatedField_to_ary(VALUE _self); VALUE RepeatedField_eq(VALUE _self, VALUE _other); VALUE RepeatedField_hash(VALUE _self); VALUE RepeatedField_inspect(VALUE _self); @@ -497,11 +498,6 @@ VALUE Message_encode(VALUE klass, VALUE msg_rb); VALUE Message_decode_json(VALUE klass, VALUE data); VALUE Message_encode_json(VALUE klass, VALUE msg_rb); -VALUE Google_Protobuf_encode(VALUE self, VALUE msg_rb); -VALUE Google_Protobuf_decode(VALUE self, VALUE klass, VALUE msg_rb); -VALUE Google_Protobuf_encode_json(VALUE self, VALUE msg_rb); -VALUE Google_Protobuf_decode_json(VALUE self, VALUE klass, VALUE msg_rb); - VALUE Google_Protobuf_deep_copy(VALUE self, VALUE obj); VALUE build_module_from_enumdesc(EnumDescriptor* enumdef); diff --git a/ruby/lib/google/protobuf.rb b/ruby/lib/google/protobuf.rb index 72797245f1ad..99b17929f7e6 100644 --- a/ruby/lib/google/protobuf.rb +++ b/ruby/lib/google/protobuf.rb @@ -28,6 +28,9 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# require mixins before we hook them into the java & c code +require 'google/protobuf/message_exts' + if RUBY_PLATFORM == "java" require 'json' require 'google/protobuf_java' @@ -36,3 +39,25 @@ end require 'google/protobuf/repeated_field' + +module Google + module Protobuf + + def self.encode(msg) + msg.to_proto + end + + def self.encode_json(msg) + msg.to_json + end + + def self.decode(klass, proto) + klass.decode(proto) + end + + def self.decode_json(klass, json) + klass.decode_json(json) + end + + end +end diff --git a/ruby/lib/google/protobuf/message_exts.rb b/ruby/lib/google/protobuf/message_exts.rb new file mode 100644 index 000000000000..e10266ba2f36 --- /dev/null +++ b/ruby/lib/google/protobuf/message_exts.rb @@ -0,0 +1,53 @@ +# Protocol Buffers - Google's data interchange format +# Copyright 2008 Google Inc. All rights reserved. +# https://developers.google.com/protocol-buffers/ +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following disclaimer +# in the documentation and/or other materials provided with the +# distribution. +# * Neither the name of Google Inc. nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +module Google + module Protobuf + module MessageExts + + #this is only called in jruby; mri loades the ClassMethods differently + def self.included(klass) + klass.extend(ClassMethods) + end + + module ClassMethods + end + + def to_json + self.class.encode_json(self) + end + + def to_proto + self.class.encode(self) + end + + end + end +end diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java index 51c50be8e10d..dd9179b0300c 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyDescriptor.java @@ -248,6 +248,8 @@ public IRubyObject allocate(Ruby runtime, RubyClass klazz) { klass.setAllocator(allocator); klass.makeMetaClass(runtime.getObject().getMetaClass()); klass.inherit(runtime.getObject()); + RubyModule messageExts = runtime.getClassFromPath("Google::Protobuf::MessageExts"); + klass.include(new IRubyObject[] {messageExts}); klass.instance_variable_set(runtime.newString(Utils.DESCRIPTOR_INSTANCE_VAR), this); klass.defineAnnotatedMethods(RubyMessage.class); return klass; diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java index b25dc6e1d670..2d4c03b567ea 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMap.java @@ -338,7 +338,7 @@ public IRubyObject dup(ThreadContext context) { return newMap; } - @JRubyMethod(name = "to_h") + @JRubyMethod(name = {"to_h", "to_hash"}) public RubyHash toHash(ThreadContext context) { return RubyHash.newHash(context.runtime, table, context.runtime.getNil()); } diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java index c7fd7aa7bc92..547ab22cb2d6 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyMessage.java @@ -338,16 +338,20 @@ public static IRubyObject decodeJson(ThreadContext context, IRubyObject recv, IR return ret; } - @JRubyMethod(name = "to_h") + @JRubyMethod(name = {"to_h", "to_hash"}) public IRubyObject toHash(ThreadContext context) { Ruby runtime = context.runtime; RubyHash ret = RubyHash.newHash(runtime); for (Descriptors.FieldDescriptor fdef : this.descriptor.getFields()) { IRubyObject value = getField(context, fdef); - if (value.respondsTo("to_h")) { - value = Helpers.invoke(context, value, "to_h"); + if (!value.isNil()) { + if (value.respondsTo("to_h")) { + value = Helpers.invoke(context, value, "to_h"); + } else if (value.respondsTo("to_a")) { + value = Helpers.invoke(context, value, "to_a"); + } } - ret.fastASet(runtime.newString(fdef.getName()), value); + ret.fastASet(runtime.newSymbol(fdef.getName()), value); } return ret; } diff --git a/ruby/src/main/java/com/google/protobuf/jruby/RubyProtobuf.java b/ruby/src/main/java/com/google/protobuf/jruby/RubyProtobuf.java index cb3fcd48cf07..2cf210d26f58 100644 --- a/ruby/src/main/java/com/google/protobuf/jruby/RubyProtobuf.java +++ b/ruby/src/main/java/com/google/protobuf/jruby/RubyProtobuf.java @@ -48,56 +48,6 @@ public static void createProtobuf(Ruby runtime) { mProtobuf.defineAnnotatedMethods(RubyProtobuf.class); } - /* - * call-seq: - * Google::Protobuf.encode(msg) => bytes - * - * Encodes the given message object to protocol buffers wire format. This is an - * alternative to the #encode method on msg's class. - */ - @JRubyMethod(meta = true) - public static IRubyObject encode(ThreadContext context, IRubyObject self, IRubyObject message) { - return RubyMessage.encode(context, message.getMetaClass(), message); - } - - /* - * call-seq: - * Google::Protobuf.decode(class, bytes) => msg - * - * Decodes the given bytes as protocol buffers wire format under the - * interpretation given by the given class's message definition. This is an - * alternative to the #decode method on the given class. - */ - @JRubyMethod(meta = true) - public static IRubyObject decode(ThreadContext context, IRubyObject self, IRubyObject klazz, IRubyObject message) { - return RubyMessage.decode(context, klazz, message); - } - - /* - * call-seq: - * Google::Protobuf.encode_json(msg) => json_string - * - * Encodes the given message object to its JSON representation. This is an - * alternative to the #encode_json method on msg's class. - */ - @JRubyMethod(name = "encode_json", meta = true) - public static IRubyObject encodeJson(ThreadContext context, IRubyObject self, IRubyObject message) { - return RubyMessage.encodeJson(context, message.getMetaClass(), message); - } - - /* - * call-seq: - * Google::Protobuf.decode_json(class, json_string) => msg - * - * Decodes the given JSON string under the interpretation given by the given - * class's message definition. This is an alternative to the #decode_json method - * on the given class. - */ - @JRubyMethod(name = "decode_json", meta = true) - public static IRubyObject decodeJson(ThreadContext context, IRubyObject self, IRubyObject klazz, IRubyObject message) { - return RubyMessage.decodeJson(context, klazz, message); - } - /* * call-seq: * Google::Protobuf.deep_copy(obj) => copy_of_obj diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index 1c2a03dc4081..3d977c08b2e7 100644 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -822,6 +822,67 @@ def test_parse_serialize assert m == m2 end + def test_encode_decode_helpers + m = TestMessage.new(:optional_string => 'foo', :repeated_string => ['bar1', 'bar2']) + json = m.to_json + m2 = TestMessage.decode_json(json) + assert m2.optional_string == 'foo' + assert m2.repeated_string == ['bar1', 'bar2'] + + proto = m.to_proto + m2 = TestMessage.decode(proto) + assert m2.optional_string == 'foo' + assert m2.repeated_string == ['bar1', 'bar2'] + end + + def test_protobuf_encode_decode_helpers + m = TestMessage.new(:optional_string => 'foo', :repeated_string => ['bar1', 'bar2']) + encoded_msg = Google::Protobuf.encode(m) + assert_equal m.to_proto, encoded_msg + + decoded_msg = Google::Protobuf.decode(TestMessage, encoded_msg) + assert_equal TestMessage.decode(m.to_proto), decoded_msg + end + + def test_protobuf_encode_decode_json_helpers + m = TestMessage.new(:optional_string => 'foo', :repeated_string => ['bar1', 'bar2']) + encoded_msg = Google::Protobuf.encode_json(m) + assert_equal m.to_json, encoded_msg + + decoded_msg = Google::Protobuf.decode_json(TestMessage, encoded_msg) + assert_equal TestMessage.decode_json(m.to_json), decoded_msg + end + + def test_to_h + m = TestMessage.new(:optional_bool => true, :optional_double => -10.100001, :optional_string => 'foo', :repeated_string => ['bar1', 'bar2']) + expected_result = { + :optional_bool=>true, + :optional_bytes=>"", + :optional_double=>-10.100001, + :optional_enum=>:Default, + :optional_float=>0.0, + :optional_int32=>0, + :optional_int64=>0, + :optional_msg=>nil, + :optional_string=>"foo", + :optional_uint32=>0, + :optional_uint64=>0, + :repeated_bool=>[], + :repeated_bytes=>[], + :repeated_double=>[], + :repeated_enum=>[], + :repeated_float=>[], + :repeated_int32=>[], + :repeated_int64=>[], + :repeated_msg=>[], + :repeated_string=>["bar1", "bar2"], + :repeated_uint32=>[], + :repeated_uint64=>[] + } + assert_equal expected_result, m.to_h + end + + def test_def_errors s = Google::Protobuf::DescriptorPool.new assert_raise TypeError do