-- proto3 JSON mapping tests. local t = require('luatest') local ffi = require('ffi') local pb = require('pb') local json = require('json') local hello = require('full.hello.hello_pb') local function reparse(s) return json.decode(s) end local g = t.group('json.scalars') g.test_basic_round_trip = function() local p = {name = 'Alice', age = 30} local enc = pb.json.encode(hello.Person_descriptor, p) local obj = reparse(enc) t.assert_equals(obj.name, 'Alice') t.assert_equals(obj.age, 30) end g.test_proto3_default_elision = function() -- Defaults are elided from JSON output unless presence is meaningful. local enc = pb.json.encode(hello.Person_descriptor, {age = 0, name = ''}) t.assert_equals(reparse(enc), setmetatable({}, getmetatable(reparse('{}')))) end g.test_camel_case_field_names = function() local enc = pb.json.encode(hello.Person_descriptor, {user_id = ffi.cast('uint64_t', 1234567890123)}) t.assert_str_contains(enc, '"userId"', 'field emitted in camelCase') end g.test_int64_as_string = function() local p = {user_id = ffi.cast('uint64_t', 12345678901234567890ULL)} local obj = reparse(pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(obj.userId, '12345678901234567890', 'uint64 stringified per spec') end g.test_bytes_base64 = function() local p = {avatar = '\x00\x01\xff'} local obj = reparse(pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(obj.avatar, 'AAH/') -- Round-trip local p2 = pb.json.decode(hello.Person_descriptor, pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(p2.avatar, p.avatar) end g.test_repeated_packed_scalar = function() local p = {lucky_numbers = {1, 2, 3}} local obj = reparse(pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(obj.luckyNumbers, {1, 2, 3}) end g.test_repeated_string = function() local p = {emails = {'a@x', 'b@x'}} local obj = reparse(pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(obj.emails, {'a@x', 'b@x'}) end g.test_enum_as_name = function() local enc = pb.json.encode(hello.Person_descriptor, {status = hello.Status.ERROR}) t.assert_str_contains(enc, '"status":"ERROR"') end g.test_enum_string_input_accepted_on_decode = function() local p = pb.json.decode(hello.Person_descriptor, '{"status":"OK"}') t.assert_equals(p.status, hello.Status.OK) end g.test_snake_case_input_also_accepted = function() local p = pb.json.decode(hello.Person_descriptor, '{"user_id":"99"}') t.assert_equals(tonumber(p.user_id), 99) end g.test_nested_message_round_trip = function() local p = {name = 'P', address = {street = 'X', zip = 1}} local enc = pb.json.encode(hello.Person_descriptor, p) local back = pb.json.decode(hello.Person_descriptor, enc) t.assert_equals(back.name, 'P') t.assert_equals(back.address.street, 'X') t.assert_equals(back.address.zip, 1) end g.test_map_round_trip = function() local p = {ages_by_nickname = {alice = 30, bob = 25}} local back = pb.json.decode(hello.Person_descriptor, pb.json.encode(hello.Person_descriptor, p)) t.assert_equals(back.ages_by_nickname.alice, 30) t.assert_equals(back.ages_by_nickname.bob, 25) end -- --------------------------------------------------------------------------- -- WKT -- --------------------------------------------------------------------------- local gwkt = t.group('json.wkt') gwkt.test_timestamp_iso_8601 = function() local datetime = require('datetime') local dt = datetime.new({timestamp = 1700000000, nsec = 123456789}) local enc = pb.json.encode(hello.Event_descriptor, {created_at = dt}) local obj = reparse(enc) -- Tarantool's datetime tostring is ISO 8601: "2023-11-14T22:13:20.123456789Z" t.assert_str_matches(obj.createdAt, '^%d%d%d%d%-%d%d%-%d%dT.+Z$') local back = pb.json.decode(hello.Event_descriptor, enc) t.assert(datetime.is_datetime(back.created_at)) t.assert_equals(back.created_at.epoch, 1700000000) t.assert_equals(back.created_at.nsec, 123456789) end gwkt.test_duration_string = function() local enc = pb.json.encode(hello.Event_descriptor, {duration = {seconds = 5, nanos = 0}}) local obj = reparse(enc) t.assert_equals(obj.duration, '5s') local enc2 = pb.json.encode(hello.Event_descriptor, {duration = {seconds = 5, nanos = 1}}) t.assert_equals(reparse(enc2).duration, '5.000000001s') local back = pb.json.decode(hello.Event_descriptor, enc2) t.assert_equals(tonumber(back.duration.seconds), 5) t.assert_equals(back.duration.nanos, 1) end gwkt.test_empty = function() local enc = pb.json.encode(hello.Event_descriptor, {ack = {}}) local obj = reparse(enc) t.assert_equals(type(obj.ack), 'table') end gwkt.test_wrappers_unwrap = function() local enc = pb.json.encode(hello.Event_descriptor, { retry_count = 5, note = 'remember', is_admin = true, }) local obj = reparse(enc) t.assert_equals(obj.retryCount, 5, 'Int32Value unwrapped on encode') t.assert_equals(obj.note, 'remember', 'StringValue unwrapped') t.assert_equals(obj.isAdmin, true, 'BoolValue unwrapped') -- Zero-value wrappers preserve presence: round-trip through JSON. local enc0 = pb.json.encode(hello.Event_descriptor, {retry_count = 0}) t.assert_equals(reparse(enc0).retryCount, 0) local back = pb.json.decode(hello.Event_descriptor, enc) t.assert_equals(back.retry_count, 5) t.assert_equals(back.note, 'remember') t.assert_equals(back.is_admin, true) end -- --------------------------------------------------------------------------- -- Struct / Value / ListValue -- --------------------------------------------------------------------------- local gsv = t.group('json.struct_value') gsv.test_struct_field_emits_object = function() local enc = pb.json.encode(hello.Event_descriptor, { payload = pb.wkt.struct({k = 'v', n = 42, on = true}), }) local obj = reparse(enc) t.assert_equals(type(obj.payload), 'table') t.assert_equals(obj.payload.k, 'v') t.assert_equals(obj.payload.n, 42) t.assert_equals(obj.payload.on, true) end gsv.test_value_field_dispatches_on_lua_type = function() local cases = { {input = 'hello', expect = 'hello'}, {input = 42, expect = 42}, {input = true, expect = true}, {input = pb.NULL, expect = box.NULL}, } for _, c in ipairs(cases) do local enc = pb.json.encode(hello.Event_descriptor, {attribute = c.input}) local obj = reparse(enc) t.assert_equals(obj.attribute, c.expect) end end gsv.test_list_value_field_emits_array = function() local enc = pb.json.encode(hello.Event_descriptor, { tags = pb.wkt.list({'alpha', 7, false, pb.NULL}), }) local obj = reparse(enc) t.assert_equals(#obj.tags, 4) t.assert_equals(obj.tags[1], 'alpha') t.assert_equals(obj.tags[2], 7) t.assert_equals(obj.tags[3], false) t.assert_equals(obj.tags[4], box.NULL) end gsv.test_struct_value_json_round_trip = function() local e = { payload = pb.wkt.struct({nested = pb.wkt.struct({k = 1})}), attribute = pb.wkt.list({'a', 'b'}), tags = pb.wkt.list({pb.NULL, true, 'x'}), } local enc = pb.json.encode(hello.Event_descriptor, e) local back = pb.json.decode(hello.Event_descriptor, enc) t.assert_equals(back.payload.nested.k, 1) t.assert_equals(back.attribute[1], 'a') t.assert_equals(back.attribute[2], 'b') t.assert_equals(back.tags[1], pb.NULL) t.assert_equals(back.tags[2], true) t.assert_equals(back.tags[3], 'x') end gsv.test_decode_json_null_is_pb_null_in_value = function() -- Top-level Value: decode a literal JSON null. local v = pb.json.decode(pb.wkt.Value_descriptor, 'null') t.assert_equals(v, pb.NULL) end -- --------------------------------------------------------------------------- -- Oneof -- --------------------------------------------------------------------------- local gone = t.group('json.oneof') gone.test_oneof_branch_emitted = function() local enc = pb.json.encode(hello.Result_descriptor, {id = 7, text = 'hi'}) local obj = reparse(enc) t.assert_equals(obj.id, 7) t.assert_equals(obj.text, 'hi') t.assert_equals(obj.code, nil) t.assert_equals(obj.details, nil) end gone.test_oneof_default_value_branch = function() -- text='' is the proto3 default for string but presence is meaningful -- inside an oneof: it must survive JSON round-trip. local enc = pb.json.encode(hello.Result_descriptor, {text = ''}) t.assert_str_contains(enc, '"text"') local back = pb.json.decode(hello.Result_descriptor, enc) t.assert_equals(back.text, '') end