~bigbes/tarantool

tarantool-protobuf

0666a6485f47f04c36fb0f7ff3db68d7b0406470 — Eugene Blikh 2 months ago d7b0364
lsp: annotate codec / grpc / json / wkt public surface (74c)

Phase-2 of the LSP / LLM affordance work (phase-1 landed annotations
on init.lua + lazy.lua). Adds ---@param / ---@return on the
public-surface entry points so editors and LLM assistants see typed
signatures on hover.

- codec.lua: encode_message, decode_message, encode_field,
  compile_writers, compile_readers, merge_message.
- grpc.lua: loopback, multiplex, new_stream_pair, wrap_call,
  wrap_server_stream, wrap_server_view.
- json.lua: M.encode, M.decode. New pb.JsonEncodeOpts and
  pb.JsonDecodeOpts @class blocks in _types.lua document the opts
  fields the implementation actually consults (use_proto_names,
  emit_defaults / always_emit_zero_value alias, indent;
  ignore_unknown_fields on decode).
- wkt.lua: register, lookup, any_pack, any_unpack. New
  pb.AnyMessage @class for the {type_url, value} shape.

Also corrects two pre-existing signature lies in _types.lua:

- pb.register is `(desc): pb.Descriptor`, not `(full_name, desc)` —
  the implementation has always derived the key from desc.name and
  all callers pass a single arg.
- pb.any.pack is `(desc, t, type_url_prefix?)`, not `(t, type_url)`.

Pure metadata — `just test` stays at 752 and `just test-c` at 1043.

bd-74c
5 files changed, 77 insertions(+), 9 deletions(-)

M runtime/pb/_types.lua
M runtime/pb/codec.lua
M runtime/pb/grpc.lua
M runtime/pb/json.lua
M runtime/pb/wkt.lua
M runtime/pb/_types.lua => runtime/pb/_types.lua +24 -9
@@ 81,8 81,8 @@
---@field codec       table                                  # internal: shared with generated code
---@field wkt         pb.Wkt
---@field NULL        userdata                               # google.protobuf.Value null sentinel
---@field register    fun(full_name: string, desc: pb.Descriptor)
---@field lookup      fun(full_name: string): pb.Descriptor?
---@field register    fun(desc: pb.Descriptor): pb.Descriptor
---@field lookup      fun(name_or_url: string): pb.Descriptor?
---@field any         pb.Any
---@field grpc        pb.Grpc
---@field c_runtime?  table                                  # set when PB_ENABLE_C=1 and load succeeds


@@ 104,18 104,33 @@
---@field finalize_message fun(desc: pb.Descriptor): pb.Descriptor
---@field register_extension fun(extendee_desc: pb.Descriptor, ext: pb.Field)

---@class pb.AnyMessage
---@field type_url string
---@field value    string

---@class pb.Any
---@field pack   fun(t: table, type_url: string): table     # returns google.protobuf.Any shape
---@field unpack fun(any_msg: table): table?, pb.Descriptor?
---@field pack   fun(desc: pb.Descriptor, t: table, type_url_prefix?: string): pb.AnyMessage
---@field unpack fun(any_msg: pb.AnyMessage, desc_override?: pb.Descriptor): table

---@class pb.Wkt
---@field NULL     userdata
---@field register fun(full_name: string, desc: pb.Descriptor)
---@field lookup   fun(full_name: string): pb.Descriptor?
---@field NULL        userdata
---@field register    fun(desc: pb.Descriptor): pb.Descriptor    # key is desc.name; also indexes the default type.googleapis.com/ URL
---@field lookup      fun(name_or_url: string): pb.Descriptor?
---@field any_pack    fun(desc: pb.Descriptor, t: table, type_url_prefix?: string): pb.AnyMessage
---@field any_unpack  fun(any_msg: pb.AnyMessage, desc_override?: pb.Descriptor): table

---@class pb.JsonEncodeOpts
---@field use_proto_names?       boolean    emit snake_case field names instead of camelCase
---@field emit_defaults?         boolean    emit fields equal to proto3 defaults (alias: always_emit_zero_value)
---@field always_emit_zero_value? boolean   deprecated alias of emit_defaults
---@field indent?                string     non-empty string ⇒ pretty-print with that indent unit

---@class pb.JsonDecodeOpts
---@field ignore_unknown_fields? boolean   silently drop unknown JSON fields instead of erroring

---@class pb.Json
---@field encode fun(desc: pb.Descriptor, t: table, opts?: table): string
---@field decode fun(desc: pb.Descriptor, s: string, opts?: table): table
---@field encode fun(desc: pb.Descriptor, t: table, opts?: pb.JsonEncodeOpts): string
---@field decode fun(desc: pb.Descriptor, s: string, opts?: pb.JsonDecodeOpts): table

---@class pb.Text
---@field encode fun(desc: pb.Descriptor, t: table, opts?: pb.TextOpts): string

M runtime/pb/codec.lua => runtime/pb/codec.lua +15 -0
@@ 80,6 80,9 @@ local decode_group
--   - sub-message fields: recursive merge
-- Sub-messages whose descriptor carries a custom decode (WKT) are replaced
-- wholesale because their decoded value is not a generic Lua table.
---@param desc    pb.Descriptor
---@param prev    table  decoded message being accumulated
---@param decoded table  newly-decoded copy to merge into `prev` in place
local function merge_message(desc, prev, decoded)
    for i = 1, #desc.fields do
        local f = desc.fields[i]


@@ 625,12 628,17 @@ end
-- without walking a full message (e.g. lazy passthrough re-encode, which
-- splices original wire segments for untouched fields and calls
-- encode_field for the dirty ones).
---@param field pb.Field
---@param value any
---@param out   string[]    table.concat-friendly chunk buffer; encoded bytes are appended
---@param force? boolean    bypass proto3 default-value elision (used for extensions and inside oneofs)
M.encode_field = function(field, value, out, force)
    return encode_field(field, value, out, force)
end

-- compile_writers attaches `f._writer` to each field where the shape is
-- specialized. Called from pb.finalize_message after the oneof flatten.
---@param desc pb.Descriptor
function M.compile_writers(desc)
    for _, f in ipairs(desc.fields) do
        f._writer = build_writer(f, desc.name)


@@ 863,12 871,16 @@ local function build_reader(f)
    return nil
end

---@param desc pb.Descriptor
function M.compile_readers(desc)
    for _, f in ipairs(desc.fields) do
        f._reader = build_reader(f)
    end
end

---@param desc pb.Descriptor
---@param data table              message contents keyed by proto field name
---@return string                 wire-format bytes (proto3 / proto2)
encode_message = function(desc, data)
    if type(data) ~= 'table' then
        error(("expected table for message %s, got %s"):format(desc.name, type(data)), 0)


@@ 1129,6 1141,9 @@ decode_extension = function(ext, buf, pos, wt, result)
    error("decode_extension: unknown kind " .. tostring(kind), 0)
end

---@param desc pb.Descriptor
---@param buf  string           wire-format bytes
---@return table                decoded message; unknown fields go in `_unknown_fields`, extensions in `_extensions`
decode_message = function(desc, buf)
    if type(buf) ~= 'string' then
        error(("expected string for decode of %s, got %s"):format(desc.name, type(buf)), 0)

M runtime/pb/grpc.lua => runtime/pb/grpc.lua +19 -0
@@ 65,6 65,10 @@ end
-- loopback to bridge an in-process server fiber with a client caller.
-- The returned `internal_state` is exposed so the transport (not the
-- caller) can flag errors and trigger close.
---@param buf_size? integer       fiber.channel capacity; defaults to DEFAULT_BUFFER
---@return table client            speaks send / close_send / recv / cancel
---@return table server            speaks recv / send / _finish / _force_close_recv
---@return table internal_state    shared {canceled, server_err} accessed by the transport
function M.new_stream_pair(buf_size)
    buf_size = buf_size or DEFAULT_BUFFER
    local c2s = fiber.channel(buf_size)  -- client -> server


@@ 189,6 193,8 @@ end
-- loopback(server) bridges an in-process M.<Service>_server(impl) result
-- into the transport contract. Streaming methods run their handler on a
-- worker fiber and communicate via fiber.channel.
---@param server pb.GrpcServer    output of `M.<Service>_server(impl)`
---@return pb.GrpcTransport
function M.loopback(server)
    if type(server) ~= 'table' or type(server.methods) ~= 'table' then
        error("pb.grpc.loopback: expected a server table from M.<Service>_server()", 0)


@@ 213,6 219,8 @@ end

-- multiplex({server1, server2, ...}) merges several servers' methods +
-- streams under a single transport. Errors on duplicate paths.
---@param servers pb.GrpcServer[]
---@return pb.GrpcTransport
function M.multiplex(servers)
    local methods, streams = {}, {}
    for _, srv in ipairs(servers) do


@@ 254,6 262,9 @@ end
-- means we can refactor the streaming surface without re-running protoc.

-- Wrap a server-streaming call: caller calls stream:recv() until nil.
---@param raw table                                       transport-side stream view (bytes)
---@param output_decode fun(bytes: string): table         per-message decoder for the typed view
---@return table                                          {recv(self): msg?, err?; cancel(self)}
function M.wrap_server_stream(raw, output_decode)
    return {
        recv = function(_)


@@ 266,6 277,10 @@ function M.wrap_server_stream(raw, output_decode)
end

-- Wrap a client-streaming or bidi call: caller sends + recvs.
---@param raw table                                       transport-side stream view (bytes)
---@param input_encode  fun(msg: table): string           per-message encoder for the typed view
---@param output_decode fun(bytes: string): table         per-message decoder for the typed view
---@return table                                          {send, close_send, recv, cancel}
function M.wrap_call(raw, input_encode, output_decode)
    return {
        send = function(_, msg)


@@ 284,6 299,10 @@ end
-- Wrap a server-side stream view for the generated server handler:
-- the user-supplied impl is called with a stream that speaks decoded
-- messages, hiding the per-message encode/decode boundary.
---@param raw table                                              server-side stream view (bytes)
---@param input_decode?  fun(bytes: string): table               decoder for inbound messages (nil ⇒ server_stream: no inbound)
---@param output_encode? fun(msg: table): string                 encoder for outbound messages (nil ⇒ client_stream: no outbound)
---@return table                                                 {recv?, send?, close_send?, cancel}
function M.wrap_server_view(raw, input_decode, output_encode)
    local wrapped = {}
    if input_decode ~= nil then

M runtime/pb/json.lua => runtime/pb/json.lua +8 -0
@@ 967,6 967,10 @@ end

to_json_value = encode_message

---@param desc pb.Descriptor
---@param t    table
---@param opts? pb.JsonEncodeOpts
---@return string
function M.encode(desc, t, opts)
    if opts ~= nil and type(opts) ~= 'table' then
        error('pb.json.encode: opts must be a table, got ' .. type(opts), 0)


@@ 1485,6 1489,10 @@ local function find_duplicate_json_keys(s)
    end
end

---@param desc pb.Descriptor
---@param s    string
---@param opts? pb.JsonDecodeOpts
---@return table
function M.decode(desc, s, opts)
    local dup_err = find_duplicate_json_keys(s)
    if dup_err ~= nil then error(dup_err, 0) end

M runtime/pb/wkt.lua => runtime/pb/wkt.lua +11 -0
@@ 546,6 546,8 @@ local function type_url_full_name(url)
    return url:match('([^/]+)$') or url
end

---@param desc pb.Descriptor
---@return pb.Descriptor
M.register = function(desc)
    if type(desc) ~= 'table' or desc.name == nil then
        error('pb.register: expected a descriptor with a `name` field', 0)


@@ 555,11 557,17 @@ M.register = function(desc)
    return desc
end

---@param name_or_url string                     bare full name (`pkg.Foo`) or a type URL (`type.googleapis.com/pkg.Foo`)
---@return pb.Descriptor?
M.lookup = function(name_or_url)
    return REGISTRY[name_or_url] or REGISTRY[type_url_full_name(name_or_url)]
end

-- Pack a Lua message table into an opaque Any form.
---@param desc pb.Descriptor
---@param t    table
---@param type_url_prefix? string                defaults to `type.googleapis.com/`
---@return pb.AnyMessage
M.any_pack = function(desc, t, type_url_prefix)
    if desc == nil or desc.name == nil then
        error('pb.any.pack: descriptor must have a `name`', 0)


@@ 571,6 579,9 @@ M.any_pack = function(desc, t, type_url_prefix)
end

-- Unpack an Any table. `desc_or_nil` overrides the registry lookup.
---@param any_t      pb.AnyMessage                google.protobuf.Any-shaped table {type_url=..., value=...}
---@param desc_or_nil? pb.Descriptor              override the registry lookup
---@return table
M.any_unpack = function(any_t, desc_or_nil)
    if type(any_t) ~= 'table' then
        error('pb.any.unpack: expected Any table', 0)