aboutsummaryrefslogtreecommitdiffhomepage
path: root/ruby
diff options
context:
space:
mode:
authorGravatar Joshua Haberman <jhaberman@gmail.com>2017-05-15 08:05:27 -0700
committerGravatar GitHub <noreply@github.com>2017-05-15 08:05:27 -0700
commitb28617b813b4ba4845164a1f5190f762f5b91832 (patch)
treee6cd363789bf163dd1dcfd6c605b7da1a107e0b2 /ruby
parent455b61c6b0f39ac269b26969877dd3c6f3e32270 (diff)
parentaec07110750924790a939826f8ac3444f22f00bf (diff)
Merge pull request #2815 from devwout/ruby_json_emit_defaults
Ruby version optionally emits default values in JSON encoding.
Diffstat (limited to 'ruby')
-rw-r--r--ruby/ext/google/protobuf_c/encode_decode.c59
-rw-r--r--ruby/tests/basic.rb123
2 files changed, 149 insertions, 33 deletions
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 34251234..02cc7a03 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 --------------
@@ -1184,21 +1185,135 @@ 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: []
+ }
+
+ actual = TestMessage.encode_json(m, :emit_defaults => true)
+
+ assert JSON.parse(actual, :symbolize_names => 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: []
+ }
+
+ actual = TestMessage.encode_json(m, :emit_defaults => true)
+
+ assert JSON.parse(actual, :symbolize_names => 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: []
+ }
+
+ actual = TestMessage.encode_json(m, :emit_defaults => true)
+
+ 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
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}}}
+
+ actual = MapMessage.encode_json(m, :emit_defaults => true)
+
+ assert JSON.parse(actual, :symbolize_names => true) == expected
+ end
+
def test_comparison_with_arbitrary_object
assert MapMessage.new != nil
end