diff --git a/runtime/pb/_types.lua b/runtime/pb/_types.lua index f346d6ca02ce6230f029e6d11b88f7d2a49caeb4..44c7eb8b100d931c3e78dd39143c33ae24708445 100644 --- a/runtime/pb/_types.lua +++ b/runtime/pb/_types.lua @@ -81,8 +81,8 @@ ---@field wire pb.Wire ---@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 enum fun(name: string, values: table): pb.EnumDescriptor ---@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 diff --git a/runtime/pb/codec.lua b/runtime/pb/codec.lua index 6724e6c2564b72ca010310049813492720b6f805..73c8a3715619f9f3c78539dce078472960645e33 100644 --- a/runtime/pb/codec.lua +++ b/runtime/pb/codec.lua @@ -80,6 +80,9 @@ -- - map fields: last-wins per key -- - 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 @@ -- Expose encode_field for callers that need to emit a single field's bytes -- 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 @@ 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 @@ end 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) diff --git a/runtime/pb/grpc.lua b/runtime/pb/grpc.lua index 83b5eb641b6041586faa8c3515720e8ca7b858a8..924bb03a323545a6b0e0bce39d9e0357fd98213a 100644 --- a/runtime/pb/grpc.lua +++ b/runtime/pb/grpc.lua @@ -65,6 +65,10 @@ -- Returns two opposed views of a bidirectional message pipe. Used by -- 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 @@ -- loopback(server) bridges an in-process M._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._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._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 @@ -- messages) facade. Living in pb.grpc keeps the generated code small and -- 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 @@ } 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 @@ -- 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 diff --git a/runtime/pb/json.lua b/runtime/pb/json.lua index 57975ff7dd38f02809cdffbe0d6b67bebd233196..e03d15624b8340a35de8737339c1204d95ca8d66 100644 --- a/runtime/pb/json.lua +++ b/runtime/pb/json.lua @@ -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 @@ end 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 diff --git a/runtime/pb/wkt.lua b/runtime/pb/wkt.lua index 22b5db3f20902397ad72974e7648fdcd0b22365b..5ac017fb409dd4d303d58a1abb8ccf2e56203b38 100644 --- a/runtime/pb/wkt.lua +++ b/runtime/pb/wkt.lua @@ -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 @@ REGISTRY[DEFAULT_PREFIX .. desc.name] = 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 @@ return {type_url = prefix .. desc.name, value = enc} 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)