From 008dc92c9d32abcf608883ae44d5a78f59a4e3c2 Mon Sep 17 00:00:00 2001 From: Ewout Date: Thu, 9 Mar 2017 20:47:56 +0100 Subject: Ruby version optionally emits default values in JSON encoding. Usage: Message.encode_json(m, emit_defaults: true) Message fields that are nil will still not appear in the encoded JSON. --- ruby/ext/google/protobuf_c/encode_decode.c | 59 +++++++++++++++--------------- ruby/tests/basic.rb | 38 +++++++++++++++++++ 2 files changed, 68 insertions(+), 29 deletions(-) (limited to 'ruby') diff --git a/ruby/ext/google/protobuf_c/encode_decode.c b/ruby/ext/google/protobuf_c/encode_decode.c index d86a1145..6ce6d083 100644 --- a/ruby/ext/google/protobuf_c/encode_decode.c +++ b/ruby/ext/google/protobuf_c/encode_decode.c @@ -914,13 +914,9 @@ void stringsink_uninit(stringsink *sink) { // semantics, which means that we have true field presence, we will want to // modify msgvisitor so that it emits all present fields rather than all // non-default-value fields. -// -// Likewise, when implementing JSON serialization, we may need to have a -// 'verbose' mode that outputs all fields and a 'concise' mode that outputs only -// those with non-default values. static void putmsg(VALUE msg, const Descriptor* desc, - upb_sink *sink, int depth); + upb_sink *sink, int depth, bool emit_defaults); static upb_selector_t getsel(const upb_fielddef *f, upb_handlertype_t type) { upb_selector_t ret; @@ -952,7 +948,7 @@ static void putstr(VALUE str, const upb_fielddef *f, upb_sink *sink) { } static void putsubmsg(VALUE submsg, const upb_fielddef *f, upb_sink *sink, - int depth) { + int depth, bool emit_defaults) { upb_sink subsink; VALUE descriptor; Descriptor* subdesc; @@ -963,12 +959,12 @@ static void putsubmsg(VALUE submsg, const upb_fielddef *f, upb_sink *sink, subdesc = ruby_to_Descriptor(descriptor); upb_sink_startsubmsg(sink, getsel(f, UPB_HANDLER_STARTSUBMSG), &subsink); - putmsg(submsg, subdesc, &subsink, depth + 1); + putmsg(submsg, subdesc, &subsink, depth + 1, emit_defaults); upb_sink_endsubmsg(sink, getsel(f, UPB_HANDLER_ENDSUBMSG)); } static void putary(VALUE ary, const upb_fielddef *f, upb_sink *sink, - int depth) { + int depth, bool emit_defaults) { upb_sink subsink; upb_fieldtype_t type = upb_fielddef_type(f); upb_selector_t sel = 0; @@ -1005,7 +1001,7 @@ static void putary(VALUE ary, const upb_fielddef *f, upb_sink *sink, putstr(*((VALUE *)memory), f, &subsink); break; case UPB_TYPE_MESSAGE: - putsubmsg(*((VALUE *)memory), f, &subsink, depth); + putsubmsg(*((VALUE *)memory), f, &subsink, depth, emit_defaults); break; #undef T @@ -1019,7 +1015,8 @@ static void put_ruby_value(VALUE value, const upb_fielddef *f, VALUE type_class, int depth, - upb_sink *sink) { + upb_sink *sink, + bool emit_defaults) { upb_selector_t sel = 0; if (upb_fielddef_isprimitive(f)) { sel = getsel(f, upb_handlers_getprimitivehandlertype(f)); @@ -1059,12 +1056,12 @@ static void put_ruby_value(VALUE value, putstr(value, f, sink); break; case UPB_TYPE_MESSAGE: - putsubmsg(value, f, sink, depth); + putsubmsg(value, f, sink, depth, emit_defaults); } } static void putmap(VALUE map, const upb_fielddef *f, upb_sink *sink, - int depth) { + int depth, bool emit_defaults) { Map* self; upb_sink subsink; const upb_fielddef* key_field; @@ -1090,9 +1087,9 @@ static void putmap(VALUE map, const upb_fielddef *f, upb_sink *sink, &entry_sink); upb_sink_startmsg(&entry_sink); - put_ruby_value(key, key_field, Qnil, depth + 1, &entry_sink); + put_ruby_value(key, key_field, Qnil, depth + 1, &entry_sink, emit_defaults); put_ruby_value(value, value_field, self->value_type_class, depth + 1, - &entry_sink); + &entry_sink, emit_defaults); upb_sink_endmsg(&entry_sink, &status); upb_sink_endsubmsg(&subsink, getsel(f, UPB_HANDLER_ENDSUBMSG)); @@ -1102,7 +1099,7 @@ static void putmap(VALUE map, const upb_fielddef *f, upb_sink *sink, } static void putmsg(VALUE msg_rb, const Descriptor* desc, - upb_sink *sink, int depth) { + upb_sink *sink, int depth, bool emit_defaults) { MessageHeader* msg; upb_msg_field_iter i; upb_status status; @@ -1144,31 +1141,31 @@ static void putmsg(VALUE msg_rb, const Descriptor* desc, if (is_map_field(f)) { VALUE map = DEREF(msg, offset, VALUE); - if (map != Qnil) { - putmap(map, f, sink, depth); + if (map != Qnil || emit_defaults) { + putmap(map, f, sink, depth, emit_defaults); } } else if (upb_fielddef_isseq(f)) { VALUE ary = DEREF(msg, offset, VALUE); if (ary != Qnil) { - putary(ary, f, sink, depth); + putary(ary, f, sink, depth, emit_defaults); } } else if (upb_fielddef_isstring(f)) { VALUE str = DEREF(msg, offset, VALUE); - if (is_matching_oneof || RSTRING_LEN(str) > 0) { + if (is_matching_oneof || emit_defaults || RSTRING_LEN(str) > 0) { putstr(str, f, sink); } } else if (upb_fielddef_issubmsg(f)) { - putsubmsg(DEREF(msg, offset, VALUE), f, sink, depth); + putsubmsg(DEREF(msg, offset, VALUE), f, sink, depth, emit_defaults); } else { upb_selector_t sel = getsel(f, upb_handlers_getprimitivehandlertype(f)); -#define T(upbtypeconst, upbtype, ctype, default_value) \ - case upbtypeconst: { \ - ctype value = DEREF(msg, offset, ctype); \ - if (is_matching_oneof || value != default_value) { \ - upb_sink_put##upbtype(sink, sel, value); \ - } \ - } \ +#define T(upbtypeconst, upbtype, ctype, default_value) \ + case upbtypeconst: { \ + ctype value = DEREF(msg, offset, ctype); \ + if (is_matching_oneof || emit_defaults || value != default_value) { \ + upb_sink_put##upbtype(sink, sel, value); \ + } \ + } \ break; switch (upb_fielddef_type(f)) { @@ -1246,7 +1243,7 @@ VALUE Message_encode(VALUE klass, VALUE msg_rb) { stackenv_init(&se, "Error occurred during encoding: %s"); encoder = upb_pb_encoder_create(&se.env, serialize_handlers, &sink.sink); - putmsg(msg_rb, desc, upb_pb_encoder_input(encoder), 0); + putmsg(msg_rb, desc, upb_pb_encoder_input(encoder), 0, false); ret = rb_str_new(sink.ptr, sink.len); @@ -1268,6 +1265,7 @@ VALUE Message_encode_json(int argc, VALUE* argv, VALUE klass) { Descriptor* desc = ruby_to_Descriptor(descriptor); VALUE msg_rb; VALUE preserve_proto_fieldnames = Qfalse; + VALUE emit_defaults = Qfalse; stringsink sink; if (argc < 1 || argc > 2) { @@ -1283,6 +1281,9 @@ VALUE Message_encode_json(int argc, VALUE* argv, VALUE klass) { } preserve_proto_fieldnames = rb_hash_lookup2( hash_args, ID2SYM(rb_intern("preserve_proto_fieldnames")), Qfalse); + + emit_defaults = rb_hash_lookup2( + hash_args, ID2SYM(rb_intern("emit_defaults")), Qfalse); } stringsink_init(&sink); @@ -1297,7 +1298,7 @@ VALUE Message_encode_json(int argc, VALUE* argv, VALUE klass) { stackenv_init(&se, "Error occurred during encoding: %s"); printer = upb_json_printer_create(&se.env, serialize_handlers, &sink.sink); - putmsg(msg_rb, desc, upb_json_printer_input(printer), 0); + putmsg(msg_rb, desc, upb_json_printer_input(printer), 0, RTEST(emit_defaults)); ret = rb_enc_str_new(sink.ptr, sink.len, rb_utf8_encoding()); diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index ca81e3a5..367be167 100644 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -1174,6 +1174,36 @@ module BasicTest Foo.encode_json(Foo.new(bar: bar, baz: [baz1, baz2])) end + def test_json_emit_defaults + # TODO: Fix JSON in JRuby version. + return if RUBY_PLATFORM == "java" + m = TestMessage.new + + expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[],"repeatedEnum":[]}' + + assert TestMessage.encode_json(m, :emit_defaults => true) == expected + end + + def test_json_emit_defaults_submsg + # TODO: Fix JSON in JRuby version. + return if RUBY_PLATFORM == "java" + m = TestMessage.new(optional_msg: TestMessage2.new) + + expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalMsg":{"foo":0},"optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[],"repeatedEnum":[]}' + + assert TestMessage.encode_json(m, :emit_defaults => true) == expected + end + + def test_json_emit_defaults_repeated_submsg + # TODO: Fix JSON in JRuby version. + return if RUBY_PLATFORM == "java" + m = TestMessage.new(repeated_msg: [TestMessage2.new]) + + expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[{"foo":0}],"repeatedEnum":[]}' + + assert TestMessage.encode_json(m, :emit_defaults => true) == expected + end + def test_json_maps # TODO: Fix JSON in JRuby version. return if RUBY_PLATFORM == "java" @@ -1189,6 +1219,14 @@ module BasicTest assert m == m2 end + def test_json_maps_emit_defaults_submsg + # TODO: Fix JSON in JRuby version. + return if RUBY_PLATFORM == "java" + m = MapMessage.new(:map_string_msg => {"a" => TestMessage2.new}) + expected = '{"mapStringInt32":{},"mapStringMsg":{"a":{"foo":0}}}' + assert MapMessage.encode_json(m, :emit_defaults => true) == expected + end + def test_comparison_with_arbitrary_object assert MapMessage.new != nil end -- cgit v1.2.3 From aec07110750924790a939826f8ac3444f22f00bf Mon Sep 17 00:00:00 2001 From: Ewout Date: Fri, 17 Mar 2017 10:28:17 +0100 Subject: Ruby tests compare parsed JSON instead of raw JSON --- ruby/tests/basic.rb | 101 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 89 insertions(+), 12 deletions(-) (limited to 'ruby') diff --git a/ruby/tests/basic.rb b/ruby/tests/basic.rb index 367be167..d9df7a5a 100644 --- a/ruby/tests/basic.rb +++ b/ruby/tests/basic.rb @@ -1,6 +1,7 @@ #!/usr/bin/ruby require 'google/protobuf' +require 'json' require 'test/unit' # ------------- generated code -------------- @@ -1179,9 +1180,33 @@ module BasicTest return if RUBY_PLATFORM == "java" m = TestMessage.new - expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[],"repeatedEnum":[]}' + expected = { + optionalInt32: 0, + optionalInt64: 0, + optionalUint32: 0, + optionalUint64: 0, + optionalBool: false, + optionalFloat: 0, + optionalDouble: 0, + optionalString: "", + optionalBytes: "", + optionalEnum: "Default", + repeatedInt32: [], + repeatedInt64: [], + repeatedUint32: [], + repeatedUint64: [], + repeatedBool: [], + repeatedFloat: [], + repeatedDouble: [], + repeatedString: [], + repeatedBytes: [], + repeatedMsg: [], + repeatedEnum: [] + } + + actual = TestMessage.encode_json(m, :emit_defaults => true) - assert TestMessage.encode_json(m, :emit_defaults => true) == expected + assert JSON.parse(actual, :symbolize_names => true) == expected end def test_json_emit_defaults_submsg @@ -1189,9 +1214,34 @@ module BasicTest return if RUBY_PLATFORM == "java" m = TestMessage.new(optional_msg: TestMessage2.new) - expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalMsg":{"foo":0},"optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[],"repeatedEnum":[]}' + expected = { + optionalInt32: 0, + optionalInt64: 0, + optionalUint32: 0, + optionalUint64: 0, + optionalBool: false, + optionalFloat: 0, + optionalDouble: 0, + optionalString: "", + optionalBytes: "", + optionalMsg: {foo: 0}, + optionalEnum: "Default", + repeatedInt32: [], + repeatedInt64: [], + repeatedUint32: [], + repeatedUint64: [], + repeatedBool: [], + repeatedFloat: [], + repeatedDouble: [], + repeatedString: [], + repeatedBytes: [], + repeatedMsg: [], + repeatedEnum: [] + } + + actual = TestMessage.encode_json(m, :emit_defaults => true) - assert TestMessage.encode_json(m, :emit_defaults => true) == expected + assert JSON.parse(actual, :symbolize_names => true) == expected end def test_json_emit_defaults_repeated_submsg @@ -1199,21 +1249,45 @@ module BasicTest return if RUBY_PLATFORM == "java" m = TestMessage.new(repeated_msg: [TestMessage2.new]) - expected = '{"optionalInt32":0,"optionalInt64":0,"optionalUint32":0,"optionalUint64":0,"optionalBool":false,"optionalFloat":0,"optionalDouble":0,"optionalString":"","optionalBytes":"","optionalEnum":"Default","repeatedInt32":[],"repeatedInt64":[],"repeatedUint32":[],"repeatedUint64":[],"repeatedBool":[],"repeatedFloat":[],"repeatedDouble":[],"repeatedString":[],"repeatedBytes":[],"repeatedMsg":[{"foo":0}],"repeatedEnum":[]}' + expected = { + optionalInt32: 0, + optionalInt64: 0, + optionalUint32: 0, + optionalUint64: 0, + optionalBool: false, + optionalFloat: 0, + optionalDouble: 0, + optionalString: "", + optionalBytes: "", + optionalEnum: "Default", + repeatedInt32: [], + repeatedInt64: [], + repeatedUint32: [], + repeatedUint64: [], + repeatedBool: [], + repeatedFloat: [], + repeatedDouble: [], + repeatedString: [], + repeatedBytes: [], + repeatedMsg: [{foo: 0}], + repeatedEnum: [] + } + + actual = TestMessage.encode_json(m, :emit_defaults => true) - assert TestMessage.encode_json(m, :emit_defaults => true) == expected + assert JSON.parse(actual, :symbolize_names => true) == expected end def test_json_maps # TODO: Fix JSON in JRuby version. return if RUBY_PLATFORM == "java" m = MapMessage.new(:map_string_int32 => {"a" => 1}) - expected = '{"mapStringInt32":{"a":1},"mapStringMsg":{}}' - expected_preserve = '{"map_string_int32":{"a":1},"map_string_msg":{}}' - assert MapMessage.encode_json(m) == expected + expected = {mapStringInt32: {a: 1}, mapStringMsg: {}} + expected_preserve = {map_string_int32: {a: 1}, map_string_msg: {}} + assert JSON.parse(MapMessage.encode_json(m), :symbolize_names => true) == expected json = MapMessage.encode_json(m, :preserve_proto_fieldnames => true) - assert json == expected_preserve + assert JSON.parse(json, :symbolize_names => true) == expected_preserve m2 = MapMessage.decode_json(MapMessage.encode_json(m)) assert m == m2 @@ -1223,8 +1297,11 @@ module BasicTest # TODO: Fix JSON in JRuby version. return if RUBY_PLATFORM == "java" m = MapMessage.new(:map_string_msg => {"a" => TestMessage2.new}) - expected = '{"mapStringInt32":{},"mapStringMsg":{"a":{"foo":0}}}' - assert MapMessage.encode_json(m, :emit_defaults => true) == expected + expected = {mapStringInt32: {}, mapStringMsg: {a: {foo: 0}}} + + actual = MapMessage.encode_json(m, :emit_defaults => true) + + assert JSON.parse(actual, :symbolize_names => true) == expected end def test_comparison_with_arbitrary_object -- cgit v1.2.3