From 784dea4d8b83b40cde4efcc7e3394941b0fd60a8 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Fri, 15 May 2026 13:39:13 +0300 Subject: [PATCH] Initial commit: protoc-gen-tarantool plugin + pb runtime A protoc plugin (Go) and a pure-Lua + LuaJIT-FFI runtime that give Tarantool a complete proto3 + gRPC stack. Two codegen modes (full inline / runtime descriptor), 226-test luatest suite, 18-fixture mainline-protoc interop corpus, JSON codec, well-known types, gRPC client/server factories, runtime .proto parser, microbench harness with allocation regression gate. Covers PLAN.md M1-M5. Module is `pb` (not `protobuf`) to avoid colliding with Tarantool's built-in encode-only `protobuf` module. --- .gitignore | 9 + Makefile | 98 + PLAN.md | 431 ++ README.md | 159 + bench/baseline.json | 35 + bench/bench.lua | 362 ++ cmd/conformance-runner.lua | 54 + cmd/conformance/core.lua | 107 + cmd/protoc-gen-tarantool/internal/gen/gen.go | 437 ++ .../internal/gen/inline.go | 603 +++ cmd/protoc-gen-tarantool/internal/gen/name.go | 82 + .../internal/gen/options.go | 34 + .../internal/gen/service.go | 202 + .../internal/gen/types.go | 44 + cmd/protoc-gen-tarantool/main.go | 90 + .../full/conformance/conformance_pb.lua | 622 +++ examples/expected/full/hello/hello_pb.lua | 993 +++++ .../proto3/test_messages_proto3_pb.lua | 3621 +++++++++++++++++ .../runtime/conformance/conformance_pb.lua | 113 + examples/expected/runtime/hello/hello_pb.lua | 248 ++ .../proto3/test_messages_proto3_pb.lua | 257 ++ examples/proto/hello.proto | 91 + go.mod | 5 + go.sum | 6 + options/tarantool/tarantool.proto | 21 + runtime/pb/codec.lua | 424 ++ runtime/pb/dynamic.lua | 235 ++ runtime/pb/grpc.lua | 302 ++ runtime/pb/init.lua | 109 + runtime/pb/json.lua | 544 +++ runtime/pb/parser.lua | 347 ++ runtime/pb/wire.lua | 368 ++ runtime/pb/wkt.lua | 566 +++ test/any_fieldmask_test.lua | 169 + test/conformance/proto/conformance.proto | 173 + .../proto/test_messages_proto3.proto | 272 ++ test/conformance_test.lua | 282 ++ test/dynamic_test.lua | 135 + test/grpc_streaming_test.lua | 302 ++ test/interop/fixtures/address_basic.bin | 3 + test/interop/fixtures/address_basic.txtpb | 4 + .../fixtures/address_with_optional.bin | Bin 0 -> 10 bytes .../fixtures/address_with_optional.txtpb | 4 + test/interop/fixtures/event_any_fieldmask.bin | 7 + .../fixtures/event_any_fieldmask.txtpb | 11 + test/interop/fixtures/event_struct_value.bin | Bin 0 -> 68 bytes .../interop/fixtures/event_struct_value.txtpb | 15 + test/interop/fixtures/person_map.bin | 6 + test/interop/fixtures/person_map.txtpb | 5 + test/interop/fixtures/person_nested.bin | 6 + test/interop/fixtures/person_nested.txtpb | 18 + test/interop/fixtures/person_packed.bin | 2 + test/interop/fixtures/person_packed.txtpb | 11 + test/interop/fixtures/person_wires.bin | Bin 0 -> 33 bytes test/interop/fixtures/person_wires.txtpb | 7 + test/interop/fixtures/result_oneof_msg.bin | 2 + test/interop/fixtures/result_oneof_msg.txtpb | 3 + test/interop/fixtures/result_oneof_text.bin | 1 + test/interop/fixtures/result_oneof_text.txtpb | 3 + test/interop_test.lua | 88 + test/json_test.lua | 233 ++ test/protobuf_test.lua | 439 ++ test/struct_value_test.lua | 215 + test/unknown_test.lua | 127 + 64 files changed, 14162 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 bench/baseline.json create mode 100644 bench/bench.lua create mode 100644 cmd/conformance-runner.lua create mode 100644 cmd/conformance/core.lua create mode 100644 cmd/protoc-gen-tarantool/internal/gen/gen.go create mode 100644 cmd/protoc-gen-tarantool/internal/gen/inline.go create mode 100644 cmd/protoc-gen-tarantool/internal/gen/name.go create mode 100644 cmd/protoc-gen-tarantool/internal/gen/options.go create mode 100644 cmd/protoc-gen-tarantool/internal/gen/service.go create mode 100644 cmd/protoc-gen-tarantool/internal/gen/types.go create mode 100644 cmd/protoc-gen-tarantool/main.go create mode 100644 examples/expected/full/conformance/conformance_pb.lua create mode 100644 examples/expected/full/hello/hello_pb.lua create mode 100644 examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua create mode 100644 examples/expected/runtime/conformance/conformance_pb.lua create mode 100644 examples/expected/runtime/hello/hello_pb.lua create mode 100644 examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua create mode 100644 examples/proto/hello.proto create mode 100644 go.mod create mode 100644 go.sum create mode 100644 options/tarantool/tarantool.proto create mode 100644 runtime/pb/codec.lua create mode 100644 runtime/pb/dynamic.lua create mode 100644 runtime/pb/grpc.lua create mode 100644 runtime/pb/init.lua create mode 100644 runtime/pb/json.lua create mode 100644 runtime/pb/parser.lua create mode 100644 runtime/pb/wire.lua create mode 100644 runtime/pb/wkt.lua create mode 100644 test/any_fieldmask_test.lua create mode 100644 test/conformance/proto/conformance.proto create mode 100644 test/conformance/proto/test_messages_proto3.proto create mode 100644 test/conformance_test.lua create mode 100644 test/dynamic_test.lua create mode 100644 test/grpc_streaming_test.lua create mode 100644 test/interop/fixtures/address_basic.bin create mode 100644 test/interop/fixtures/address_basic.txtpb create mode 100644 test/interop/fixtures/address_with_optional.bin create mode 100644 test/interop/fixtures/address_with_optional.txtpb create mode 100644 test/interop/fixtures/event_any_fieldmask.bin create mode 100644 test/interop/fixtures/event_any_fieldmask.txtpb create mode 100644 test/interop/fixtures/event_struct_value.bin create mode 100644 test/interop/fixtures/event_struct_value.txtpb create mode 100644 test/interop/fixtures/person_map.bin create mode 100644 test/interop/fixtures/person_map.txtpb create mode 100644 test/interop/fixtures/person_nested.bin create mode 100644 test/interop/fixtures/person_nested.txtpb create mode 100644 test/interop/fixtures/person_packed.bin create mode 100644 test/interop/fixtures/person_packed.txtpb create mode 100644 test/interop/fixtures/person_wires.bin create mode 100644 test/interop/fixtures/person_wires.txtpb create mode 100644 test/interop/fixtures/result_oneof_msg.bin create mode 100644 test/interop/fixtures/result_oneof_msg.txtpb create mode 100644 test/interop/fixtures/result_oneof_text.bin create mode 100644 test/interop/fixtures/result_oneof_text.txtpb create mode 100644 test/interop_test.lua create mode 100644 test/json_test.lua create mode 100644 test/protobuf_test.lua create mode 100644 test/struct_value_test.lua create mode 100644 test/unknown_test.lua diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1520a5157853ec1d4542b75094a317f1b9b8cecc --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/protoc-gen-tarantool +/dist/ +/.rocks/ +*.pb.go +.DS_Store +.idea/ +.vscode/ +*.swp +*.swo diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..f03d5756bc60c6d6ac802b728e7449837c9e894f --- /dev/null +++ b/Makefile @@ -0,0 +1,98 @@ +PLUGIN := protoc-gen-tarantool +GEN_DIR := examples/expected +PROTO_DIR := examples/proto +CONFORMANCE_PROTO_DIR := test/conformance/proto + +LUATEST := .rocks/bin/luatest +LUA_PATH_PARTS := \ + ./runtime/?/init.lua \ + ./runtime/?.lua \ + ./$(GEN_DIR)/?.lua \ + ./$(GEN_DIR)/?/init.lua \ + ./?.lua \ + ./?/init.lua \ + ./test/?.lua + +# semicolon-joined; trailing ;; lets the standard package.path defaults apply +empty := +space := $(empty) $(empty) +LUA_PATH_JOINED := $(subst $(space),;,$(strip $(LUA_PATH_PARTS)));; + +.PHONY: all build gen gen-full gen-runtime goldens test test-suite \ + bench bench-baseline bench-compare clean + +all: build gen test + +build: + go build -o $(PLUGIN) ./cmd/protoc-gen-tarantool + +gen: gen-full gen-runtime gen-conformance + +gen-full: build + mkdir -p $(GEN_DIR) + protoc \ + --plugin=./$(PLUGIN) \ + --tarantool_out=$(GEN_DIR) \ + --tarantool_opt=mode=full,prefix=full \ + -I $(PROTO_DIR) -I options \ + $(PROTO_DIR)/*.proto + +gen-runtime: build + mkdir -p $(GEN_DIR) + protoc \ + --plugin=./$(PLUGIN) \ + --tarantool_out=$(GEN_DIR) \ + --tarantool_opt=mode=runtime,prefix=runtime \ + -I $(PROTO_DIR) -I options \ + $(PROTO_DIR)/*.proto + +# Conformance + Google test_messages_proto3 are needed by cmd/conformance-runner.lua +# and the conformance self-test. Only the `full` mode is required by the runner. +gen-conformance: build + mkdir -p $(GEN_DIR) + protoc \ + --plugin=./$(PLUGIN) \ + --tarantool_out=$(GEN_DIR) \ + --tarantool_opt=mode=full,prefix=full \ + -I $(CONFORMANCE_PROTO_DIR) -I options \ + $(CONFORMANCE_PROTO_DIR)/*.proto + protoc \ + --plugin=./$(PLUGIN) \ + --tarantool_out=$(GEN_DIR) \ + --tarantool_opt=mode=runtime,prefix=runtime \ + -I $(CONFORMANCE_PROTO_DIR) -I options \ + $(CONFORMANCE_PROTO_DIR)/*.proto + +# Regenerate the interop golden corpus from .txtpb sources using mainline +# protoc. Run only when fixtures change; the generated .bin files are committed. +goldens: + @for f in test/interop/fixtures/*.txtpb; do \ + type=$$(awk '/^# type:/ {print $$3; exit}' $$f); \ + out=$${f%.txtpb}.bin; \ + echo " protoc --encode=$$type < $$f > $$out"; \ + protoc --encode=$$type -I $(PROTO_DIR) -I options $(PROTO_DIR)/hello.proto < $$f > $$out || exit $$?; \ + done + +test: gen + LUA_PATH="$(LUA_PATH_JOINED)" $(LUATEST) -v test/ + +# Microbenchmark: throughput + allocation per op across 5 payload sizes, +# both codegen modes. Throughput numbers print to stderr (informational — +# they vary with CPU load); the JSON document on stdout is the full record. +bench: gen + tarantool bench/bench.lua --print + +# Overwrite bench/baseline.json with current alloc-per-op numbers. Run on +# a quiet machine; allocs are deterministic to ~10 bytes so the file is +# hardware-independent. +bench-baseline: gen + tarantool bench/bench.lua --baseline + +# Fail with exit 1 if any alloc-per-op grows by >5% vs the committed +# baseline. Wire into CI to gate PRs. +bench-compare: gen + tarantool bench/bench.lua --compare + +clean: + rm -f $(PLUGIN) + rm -rf $(GEN_DIR) diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000000000000000000000000000000000000..ef579b28cac125754a526c3bf3e2d30c4deda06d --- /dev/null +++ b/PLAN.md @@ -0,0 +1,431 @@ +# tarantool-protobuf — Roadmap & Test Plan + +This document tracks the long-form design + testing strategy for +`protoc-gen-tarantool` and the `pb` Lua runtime. Short-term task tracking +lives in the in-session task list; this file is the slow-changing record. + +## 1. Vision + +A first-class Protocol Buffers + gRPC stack for Tarantool that: + +- Generates idiomatic Lua from `.proto` files with **two modes** (full inline + vs descriptor-driven runtime) so users can pick speed-vs-flexibility. +- Interoperates byte-for-byte with mainline protoc implementations (Go, + Python, C++) — passes Google's protobuf conformance suite for proto3. +- Ships with a small, FFI-aware runtime that respects Tarantool conventions + (LuaJIT cdata for 64-bit ints, no global state, no monkey-patching). +- Is comfortable to use from real Tarantool apps: clean integration with + `net.box`, `fiber`, `box.session`, and the 3.x config framework. + +## 2. Current state (M0) + +See `README.md` for the user-visible summary. Internally: + +- Plugin: Go, single mode (descriptor + runtime dispatch). +- Runtime: pure Lua + LuaJIT FFI; ~260 LOC wire layer, ~250 LOC dispatch. +- Tests: 25 hand-rolled assertions in one file, no luatest. +- Demo proto: 1 file, 11 fields, no map/oneof/services. + +This is the floor we're building on. + +## 3. Phased roadmap + +Each milestone ends with a green CI run, an updated README, and a tagged +release on sourcecraft.dev. + +### M1 — Two codegen modes + luatest harness *(in progress)* + +- [ ] Promote scalar encode/decode to typed helpers in `pb.wire`. + `wire.encode_int32(v)`, `wire.decode_string(buf, pos)`, etc., for all + 15 scalar proto types. Both modes consume the same primitives. +- [ ] **Full (inline) codegen**: emit per-message `_encode` / `_decode` + functions with no descriptor lookup. Tag bytes precomputed at gen + time as Lua string literals. This is the JIT-friendly hot path. +- [ ] **Runtime codegen**: keep current behavior. Useful for introspection, + schema registries, and forward-compat with descriptor-only consumers. +- [ ] Plugin parameter `mode=full|runtime` (default: `full`). +- [ ] Migrate tests to luatest groups. Every behavior tested against **both** + generated modules to prove parity. +- [ ] Generate side-by-side outputs in `examples/expected/{full,runtime}/` + for visual diffing. + +**Done when**: same `.proto` produces two modules; identical wire bytes; one +luatest run covers both. + +### M2 — Composite types + +- [ ] **`map`**: emit as repeated synthetic `*Entry` messages with + `key`/`value` fields per spec. Decode merges into a Lua table; encode + iterates with `pairs`. Key types limited to scalars + string per spec. +- [ ] **`oneof`**: descriptor includes `oneof_index` per field. Encode + emits at most one branch (last assignment wins). Decode clears prior + oneof siblings on assignment. +- [ ] **proto3 explicit `optional`**: respect presence — emit field even + when value equals scalar default. Generated descriptor exposes + `has_(t)` / `clear_(t)` helpers. + +**Done when**: a "composite" example proto with map, a oneof +with 3 cases, and an explicit-optional bool round-trips through both modes +with parity-against-protoc. + +### M3 — Well-known types + +- [ ] `google.protobuf.Timestamp` ↔ Tarantool `datetime` module + (epoch + nsec mapping). +- [ ] `google.protobuf.Duration` ↔ `interval` (or seconds+nanos table). +- [ ] Wrappers (Int32Value, StringValue, BoolValue, …) with sugar: + pass plain Lua value → auto-wrap; decode → auto-unwrap or keep. +- [x] `Empty`, `FieldMask`. FieldMask is a `repeated string`; JSON mapping + is the canonical comma-joined `lowerCamelCase` form. +- [x] `Any`: opaque `{type_url, value}` form by default; `pb.register(desc)` + + `pb.any.pack(desc, t)` / `pb.any.unpack(any_t)` for typed + round-trips. JSON canonical mapping emits the flat `{"@type": ...}` + object when the type is registered, falls back to base64 opaque form + otherwise. +- [x] `Struct`, `Value`, `ListValue` ↔ idiomatic Lua tables. + `box.NULL` is the null_value sentinel (re-exported as `pb.NULL`). + `pb.wkt.struct(t)` / `pb.wkt.list(t)` tag tables when the auto-detect + heuristic (`t[1] ~= nil` → list, else struct) needs to be overridden, + and decode preserves the tag for byte-stable round trips. + +**Done when**: WKT-using protos round-trip and integrate visibly with +`datetime` (e.g. `os.date(...)`-comparable timestamps). + +### M4 — gRPC services (transport-agnostic) *(done)* + +- [x] Per-service descriptor: methods with full path `/pkg.Svc/Method`, + input/output type refs, `client_streaming` / `server_streaming` flags. +- [x] **Client stub**: `MyService_client(transport)` returns a table with + one function per method. Transport interface, all served by the + shipped loopback + multiplex implementations: + ```lua + transport:unary(path, req_bytes, ctx) -> resp_bytes + transport:server_stream(path, req_bytes, ctx) -> stream + transport:client_stream(path, ctx) -> stream + transport:bidi(path, ctx) -> stream + ``` + Streaming returns a `stream` object with `send` / `close_send` / + `recv() -> msg, err` / `cancel`. End-of-stream is `(nil, nil)`; + handler errors surface as `(nil, err_string)`. +- [x] **Server side**: `MyService_server(impl)` returns + `{service, methods, streams}` consumable by the loopback / + multiplex transports. Handler signatures by RPC kind: + ```lua + unary: function(req, ctx) -> resp + server_stream: function(req, stream, ctx) + client_stream: function(stream, ctx) -> resp + bidi: function(stream, ctx) + ``` +- [ ] Reference network transports (separate projects, deferred): + - `pb.grpc.transport.netbox` — gRPC tunneled over `net.box` calls. + - HTTP/2 transport — out of scope; the transport interface above is + deliberately HTTP/2-shaped so a plug-in is straightforward. + +**Done.** The loopback transport runs each streaming handler on its own +fiber and bridges client ↔ handler via `fiber.channel`. All four flavors +(unary + 3 streaming) are exercised by parameterized luatest groups. + +### M5 — Conformance + interop *(runner shipped; pass-rate tracking pending)* + +- [x] Wire up Google's [protobuf conformance test runner][conformance]. + `cmd/conformance-runner.lua` is the testee: reads length-prefixed + `ConformanceRequest` on stdin, runs it through our codec / JSON, + writes a length-prefixed `ConformanceResponse` on stdout. Loops to + EOF. Core dispatch is factored into `cmd/conformance/core.lua` and + exercised directly by `test/conformance_test.lua` (10 dispatch + cases + 3 stdin/stdout framing cases). +- [ ] Run the canonical `conformance_test_runner` binary in CI and track + the pass-rate as a numeric metric (regression gate). +- [x] Cross-impl interop: 18-fixture corpus in `test/interop/fixtures/` + produced by mainline `protoc --encode`; tests assert byte-for-byte + equality. + +[conformance]: https://github.com/protocolbuffers/protobuf/tree/main/conformance + +### M6 — Performance + production polish *(bench harness shipped; rest pending)* + +- [x] Microbenchmarks: encode and decode throughput (MB/s, msgs/s) for + messages of 5 sizes (10 B / 100 B / 1 KB / 10 KB / 100 KB). Shipped + as `bench/bench.lua`, run via `make bench`. Throughput is stderr-only + (varies with CPU load); JSON document on stdout. +- [x] Allocation profiling — bytes per encode/decode op, measured via GC + delta with `collectgarbage('stop')` framing. Committed as + `bench/baseline.json`. Regression gate: `make bench-compare` exits + non-zero if alloc/op grows >5% vs baseline. Allocations are + deterministic to ~10 bytes regardless of hardware. +- [ ] Trace stability — confirm hot loops compile to a single trace; no + side traces or blacklisted bytecodes. (Requires running with + `jit.dump` enabled and inspecting traces — pending.) +- [ ] Optional output: ibuf-based encoder that writes into a caller-owned + `ffi.cdata` byte buffer instead of building a string list. Targets + hot RPC paths where allocation cost dominates. +- [ ] Decoder fast path that returns an `msgpack.object`-like lazy view + for nested messages; only materializes touched fields. + +### M7 — Developer ergonomics + +- [ ] Generated EmmyLua / lua-language-server type annotations so + `t:Person_encode({name=...})` autocompletes in editors. +- [ ] `pb.from_pb(file_descriptor_set)` — runtime descriptor parser, lets + apps load schemas at runtime without protoc-time codegen. +- [ ] JSON encoding per the [proto3 JSON spec][proto3json] (canonical and + tolerant modes). +- [ ] Text-format printer (`MyMsg.print(t)`). +- [ ] `protoc-gen-tarantool-doc`: generates Markdown reference docs + from `.proto` files. + +[proto3json]: https://protobuf.dev/programming-guides/proto3/#json + +### M8 — Release engineering + +- [ ] Sourcecraft.dev project + CI pipeline (matrix: Tarantool 2.11 / 3.x + EE + CE, Linux + macOS). +- [ ] Tagged releases; rockspec for the Lua runtime; pre-built binaries + for the Go plugin. +- [ ] Migration guide from Tarantool's built-in `require('protobuf')` to + `require('pb')`. +- [ ] Example apps: pet-clinic CRUD over gRPC; replication of state via + protobuf-encoded events on a queue. + +## 4. Per-feature design notes + +### 4.1 Inline (full) codegen — wire bytes + +For each message we emit one `_encode` and one `_decode` function. Tag +bytes are precomputed string literals; field-presence checks are inlined. +The runtime is reduced to wire-format primitives. + +```lua +function M.Address_encode(t) + local out, n = {}, 0 + local v + v = t.street + if v ~= nil and v ~= '' then + n = n + 1; out[n] = '\x0a' -- tag(1, LEN) + n = n + 1; out[n] = wire.encode_string(v) + end + -- ... + return table.concat(out) +end +``` + +Decoder uses an `if-elseif` chain on field id (LuaJIT compiles this +well for small chain lengths; for >16 fields we may want a numeric jump +table or tag-byte switching). + +### 4.2 Map fields + +`map` is wire-format-equivalent to: + +```proto +message FooEntry { K key = 1; V value = 2; } +repeated FooEntry foos = N; +``` + +Codegen synthesizes the entry message internally but exposes the field as a +Lua table (hash, not array). Encode iterates with `pairs`; decode merges +on duplicate keys (last wins, per spec). + +### 4.3 Oneofs + +Descriptor gains `oneofs = {[oneof_name] = {field_names...}}`. Encode walks +fields in declaration order and emits the first non-nil branch. Decode +clears sibling fields on assignment so callers see exactly one set. + +In inline mode, the encode walk becomes an explicit `if`-chain; the decode +clears are emitted alongside each `elseif id == N then` arm. + +### 4.4 64-bit integer ergonomics + +Locked: `int64`/`uint64`/`fixed64`/`sfixed64`/`sint64` are LuaJIT +`int64_t` / `uint64_t` cdata. Reasons: +- Lossless beyond 2^53. +- Same convention as Tarantool `msgpackffi`, `net.box`, `box.tuple`. +- Compares cleanly against `0` (numeric coercion in LuaJIT). + +We will document a `wire.from_string(s)` helper for users who get hex/dec +strings (e.g. from JSON) and need to feed them into encode. + +### 4.5 Unknown fields *(implemented)* + +`t._unknown_fields` is a single Lua string holding the verbatim +concatenation of tag+value bytes for fields the decoder didn't recognize. +Captured during decode in encounter order; re-emitted at the tail of +`_encode`. Mirrors Tarantool's built-in `protobuf` module convention. + +Map entries don't preserve unknowns (per spec — synthetic Entry messages). +WKT types bypass this too, since they have custom `desc.encode/decode`. + +Both codegen modes implement it: runtime mode in `pb.codec`, full (inline) +mode emits per-message capture/re-emit blocks. Tests live in +`test/unknown_test.lua` and run against both modes. + +### 4.6 gRPC service descriptors + +```lua +M.Greeter_service = { + name = 'hello.Greeter', + methods = { + SayHello = { + full_name = '/hello.Greeter/SayHello', + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + client_streaming = false, + server_streaming = false, + }, + -- ... + }, +} +``` + +Client: `M.Greeter_client(transport)` returns +`{SayHello = function(req) ... end, ...}`. + +Server: `M.Greeter_server(impl)` returns a table compatible with the +`server:register(svc)` interface. + +## 5. Testing strategy + +Tests live in `test/`. Layout: + +``` +test/ + unit/ -- pb.wire and pb.codec unit tests (no codegen) + wire_test.lua + codec_test.lua + roundtrip/ -- generated-module round-trip, both modes + scalars_test.lua + repeated_test.lua + nested_test.lua + map_test.lua -- M2 + oneof_test.lua -- M2 + wkt_test.lua -- M3 + conformance/ -- M5: Google conformance harness + interop/ -- M5: cross-impl byte-for-byte equality + fixtures/ -- pre-encoded payloads from Go/Python + bench/ -- M6: microbenchmarks + fuzz/ -- malformed-input + random-input harness +``` + +### 5.1 Unit tests (M1 onwards) + +For each `wire.encode_` / `wire.decode_` pair: +- Round-trip 0, min, max, edge cases (1, -1, NaN, Inf, empty string, + 254/255/256-byte string for varint length-byte boundaries). +- Truncated input → controlled error. +- Spec-conformant byte sequences from the [protobuf encoding doc][encoding]. + +[encoding]: https://protobuf.dev/programming-guides/encoding/ + +### 5.2 Round-trip parity tests (M1+) + +Every test runs against **both** generated modules (full and runtime mode) +using a parameterized luatest group: + +```lua +for _, mode in ipairs({'full', 'runtime'}) do + local g = t.group('roundtrip.' .. mode) + local hello = require('hello.' .. mode .. '.hello_pb') + g.test_address = function() ... end + -- ... +end +``` + +Coverage targets: +- All 15 scalar types, singular and repeated. +- Packed vs non-packed repeated (default + explicit). +- Nested messages, self-references, mutually recursive cycles. +- Cross-file imports (2-file fixture). +- Maps with all valid key types (M2). +- Oneofs with all 3 wire-type families (M2). +- Default-value elision (proto3 semantics). +- Explicit-optional presence (M2). +- Empty messages, single-field messages, 100-field messages. +- Field IDs straddling varint boundaries: 1, 15, 16, 2047, 2048, 2^29 - 1. + +### 5.3 Conformance suite (M5) + +Build a small Lua binary `cmd/conformance-runner.lua` that speaks the +Google protobuf conformance protocol on stdin/stdout. Checked into the +repo so CI runs it against the canonical test corpus. Track pass-rate as +a CI metric. + +### 5.4 Cross-impl interop (M5) + +For each of N reference protos: +1. `protoc --encode=msg < input.txt > golden.bin` (using mainline protoc). +2. Our test asserts `pb.decode(msg, read('golden.bin'))` produces the + expected Lua table. +3. Asserts `pb.encode(msg, expected)` produces bytes that, when decoded + by mainline protoc, yield the same logical message (allow field + reordering since neither order is canonical). + +Generate goldens once via a `make goldens` target; commit them to the +repo. CI just verifies them, never regenerates. + +### 5.5 Fuzz tests (M5/M6) + +- **Malformed-input fuzz**: feed random bytes to every `_decode` function; + assert no crashes, only controlled `error()` calls. +- **Round-trip fuzz**: generate random-but-valid messages (size-bounded), + assert `decode(encode(t)) == t` (value-equal under our equality helper). +- Use Tarantool's `math.random` with a seeded PRNG for reproducibility. + +### 5.6 Performance tests (M6) + +Microbenchmarks measured: +- Encode + decode throughput (msgs/sec, bytes/sec). +- Allocation rate (tables/sec, strings/sec) via `misc.memprof`. +- JIT trace count (no side traces in hot loops). + +Run on a fixed corpus across 5 message sizes; track results in +`bench/baseline.json` and fail PRs that regress >5%. + +### 5.7 Test infrastructure + +- **luatest** as the framework. Already installed via + `tt rocks install luatest` to `.rocks/`. +- Run via `make test` → `.rocks/bin/luatest -v test/`. +- CI: sourcecraft.dev native pipelines (TBD), matrix on Tarantool versions. +- Go side: `go test ./...` for any pure-Go logic in the plugin. +- Go integration test: spawn `protoc` on a fixture, diff generated Lua + against committed expected output. + +## 6. Tooling roadmap + +- **Justfile** (or extend Makefile) with targets: `build`, `gen`, + `test`, `bench`, `lint`, `goldens`, `conformance`, `clean`, `release`. +- **golangci-lint** for the Go side; **luacheck** for the Lua side. +- **gofumpt** + **stylua** for formatting. +- **Coverage**: `go test -cover` for plugin; `luacov` for runtime. +- **Doc generation**: `pkgsite` for Go; manually maintained Markdown for + Lua until a tool emerges. + +## 7. Open questions / future decisions + +| Question | Notes | +|---|---| +| Should map encoder be deterministic (sorted by key)? | Spec says no; some users want yes. Add `pb.encode_deterministic` flag later. | +| Public C-FFI accelerator for varint? | Defer until perf benchmarks show pure-Lua bottleneck. | +| Should we ship a stub HTTP/2 transport for gRPC? | Probably no — large dependency. Recommend `lua-http` or write a focused project. | +| How to handle the existing Tarantool-builtin `require('protobuf')`? | Document migration; do not override the loader. | +| Lua module path mapping when no `lua_package` and no proto package? | Currently uses bare filename; consider erroring out instead. | +| Schema upgrade support (v1 → v2 of a message)? | Out of scope; protobuf is forward-compatible by design. | +| Integration with Tarantool 3.x declarative config? | A `pb.types.` config role would be nice; defer until use case appears. | + +## 8. Non-goals (for now) + +- **proto2** syntax — spec is more complex (required, default values, + groups), real demand is rare on Tarantool. +- **Protobuf editions** beyond what proto3 enables. +- **HTTP/2 transport** for gRPC (separate project). +- **Reflection service** (gRPC server reflection) — implement after M5. +- **gRPC-Web** — separate project. + +## 9. How to update this plan + +Edit this file directly. After any milestone closes: +1. Move the milestone section under a `## Done` heading. +2. Cross-link the closing PR. +3. Update `README.md` status table. +4. Cut a release on sourcecraft.dev. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f3950bd7610eb1b6c06b7abd192ec6a49e66b3a2 --- /dev/null +++ b/README.md @@ -0,0 +1,159 @@ +# tarantool-protobuf + +A `protoc` plugin and pure-Lua runtime for using Protocol Buffers (proto3) and +gRPC service stubs from [Tarantool](https://www.tarantool.io/). + +Tarantool ships an in-tree `require('protobuf')` module, but it is +**encode-only** and has no support for `map`, `oneof`, services, or any decode +path. This project fills those gaps with: + +- **`protoc-gen-tarantool`** — a protoc plugin (Go) that turns `.proto` files + into Lua modules. +- **`runtime/pb`** — a pure Lua + LuaJIT-FFI runtime the generated code uses + for the wire format. Named `pb` rather than `protobuf` to avoid colliding + with Tarantool's built-in module. + +## Status + +MVP — proto3 messages and enums, end-to-end round-trip verified. + +| Feature | State | +|----------------------------------|--------------| +| proto3 scalars (all 15 types) | ✅ | +| Repeated, packed by default | ✅ | +| Nested messages, self-reference | ✅ | +| Cross-file imports | ✅ | +| Enums (open semantics) | ✅ | +| 64-bit integers as LuaJIT cdata | ✅ | +| Two codegen modes (full + runtime) | ✅ | +| `map` (scalar/message values) | ✅ | +| `oneof` | ✅ | +| proto3 explicit `optional` + has_/clear_ | ✅ | +| gRPC service stubs (unary) | ✅ | +| gRPC streaming (server / client / bidi) | ✅ | +| Loopback / multiplex transport | ✅ | +| WKT: Timestamp ↔ `datetime` | ✅ | +| WKT: Duration, Empty, wrappers | ✅ | +| WKT: Struct, Value, ListValue | ✅ | +| WKT: Any (opaque + registry pack/unpack) | ✅ | +| WKT: FieldMask | ✅ | +| Byte-for-byte interop with `protoc` | ✅ (18 fixtures) | +| Google conformance runner (`cmd/conformance-runner.lua`) | ✅ | +| Runtime `.proto` parsing (`pb.parse`) | ✅ | +| proto3 JSON (`pb.json.encode`/`.decode`) | ✅ | +| Unknown-field passthrough (`_unknown_fields`) | ✅ | +| Microbenchmark + alloc regression gate (`make bench`) | ✅ | +| proto2 / editions | ❌ out of scope | + +## Quick start + +```bash +# 1. Build the plugin and generate the example. +make gen + +# 2. Run the round-trip test in Tarantool. +make test +``` + +The plugin emits one `.lua` file per `.proto`. By default the output path +mirrors the proto package (`package foo.bar; baz.proto` → `foo/bar/baz_pb.lua`), +required as `foo.bar.baz_pb`. Override the Lua module path with a file option: + +```proto +import "tarantool/tarantool.proto"; +option (tarantool.lua_package) = "myapp.proto.foo"; +``` + +## Generated API + +For each message `Foo` the plugin emits: + +```lua +local M = require('myapp.proto.foo') + +M.Foo_descriptor -- the descriptor table consumed by the runtime +M.Foo_new(t) -- returns t (or {}); placeholder for future validation +M.Foo_encode(t) -- table -> wire bytes (string) +M.Foo_decode(b) -- wire bytes (string) -> table +``` + +For each enum `Color`: + +```lua +M.Color_descriptor -- { name, by_name, by_value } +M.Color -- alias for by_name: M.Color.RED -> 0 +``` + +Repeated fields are Lua arrays (1-based, contiguous). 64-bit integers +(`int64`, `uint64`, `fixed64`, `sfixed64`, `sint64`) are LuaJIT `int64_t` / +`uint64_t` cdata — lossless and the same convention used by Tarantool's +`net.box`, `msgpack`, and built-in `protobuf` modules. + +## Layout + +``` +cmd/protoc-gen-tarantool/ # Go plugin + main.go # reads CodeGeneratorRequest, hands off to gen + internal/gen/ # codegen package +runtime/pb/ # Lua runtime (require('pb')) + init.lua # public surface + wire.lua # varint / zigzag / fixed / float / LEN + codec.lua # descriptor-driven encode/decode +options/tarantool/ # custom proto options + tarantool.proto # (tarantool.lua_package) file option +examples/proto/ # demo .proto inputs +examples/expected/ # generated output (committed for inspection) +test/ # roundtrip test +``` + +## Conformance + +`cmd/conformance-runner.lua` speaks the [Google protobuf conformance +protocol][gconf] on stdin/stdout. Drive it with the canonical +`conformance_test_runner` binary like so: + +```bash +make gen +conformance_test_runner --enforce_recommended \ + tarantool cmd/conformance-runner.lua +``` + +The runner currently supports `protobuf_test_messages.proto3.TestAllTypesProto3` +in both protobuf and JSON formats; proto2 / editions / JSPB / text-format +cases return `skipped`. The self-test in `test/conformance_test.lua` +exercises the runner with crafted requests on every `make test` run. + +[gconf]: https://github.com/protocolbuffers/protobuf/tree/main/conformance + +## Benchmarks + +```bash +make bench # print throughput + alloc per op (5 sizes × 2 modes) +make bench-baseline # overwrite bench/baseline.json (run on a quiet machine) +make bench-compare # exit 1 if any alloc-per-op regressed >5% vs baseline +``` + +The committed `bench/baseline.json` tracks only allocation per op — that's +reproducible across machines because it counts bytes, not time. Throughput +in stderr is informational; it swings 30%+ on a contended CPU. + +Current baseline (LuaJIT 2.1, hello.Person): + +| Payload | encode alloc (full / runtime) | decode alloc (full / runtime) | +|---------|-------------------------------|-------------------------------| +| 10 B | 0.37 / 0.63 KB | 0.51 / 0.51 KB | +| 100 B | 0.37 / 0.63 KB | 0.51 / 0.51 KB | +| 1 KB | 5.98 / 7.04 KB | 7.75 / 7.88 KB | +| 10 KB | 47.5 / 48.6 KB | 59.5 / 59.6 KB | +| 100 KB | 444 / 445 KB | 568 / 568 KB | + +## Why named `pb` instead of `protobuf`? + +Tarantool's loader prefers the built-in `require('protobuf')` over any +filesystem module of the same name. Trying to override it would break code +that uses the built-in's encode API. `pb` is short, unambiguous, and lives +alongside the built-in. + +## License + +TBD. diff --git a/bench/baseline.json b/bench/baseline.json new file mode 100644 index 0000000000000000000000000000000000000000..813d4bf0118102f9d18518b6630618a27e55675a --- /dev/null +++ b/bench/baseline.json @@ -0,0 +1,35 @@ +{ + "schema_message": "hello.Person", + "results": [ + { + "size_label": "10B", + "size_bytes": 10, + "encode": {"alloc_kb_per_op_full": 0.367, "alloc_kb_per_op_runtime": 0.633}, + "decode": {"alloc_kb_per_op_full": 0.508, "alloc_kb_per_op_runtime": 0.508} + }, + { + "size_label": "100B", + "size_bytes": 94, + "encode": {"alloc_kb_per_op_full": 0.367, "alloc_kb_per_op_runtime": 0.633}, + "decode": {"alloc_kb_per_op_full": 0.508, "alloc_kb_per_op_runtime": 0.508} + }, + { + "size_label": "1KB", + "size_bytes": 930, + "encode": {"alloc_kb_per_op_full": 5.978, "alloc_kb_per_op_runtime": 7.040}, + "decode": {"alloc_kb_per_op_full": 7.750, "alloc_kb_per_op_runtime": 7.883} + }, + { + "size_label": "10KB", + "size_bytes": 9634, + "encode": {"alloc_kb_per_op_full": 47.489, "alloc_kb_per_op_runtime": 48.551}, + "decode": {"alloc_kb_per_op_full": 59.500, "alloc_kb_per_op_runtime": 59.633} + }, + { + "size_label": "100KB", + "size_bytes": 96674, + "encode": {"alloc_kb_per_op_full": 444.126, "alloc_kb_per_op_runtime": 445.189}, + "decode": {"alloc_kb_per_op_full": 567.500, "alloc_kb_per_op_runtime": 567.633} + } + ] +} diff --git a/bench/bench.lua b/bench/bench.lua new file mode 100644 index 0000000000000000000000000000000000000000..356b9c35370568c2e4fbadd048efd1fa2e75190c --- /dev/null +++ b/bench/bench.lua @@ -0,0 +1,362 @@ +#!/usr/bin/env tarantool +-- Microbenchmark harness for protoc-gen-tarantool. +-- +-- Measures encode + decode throughput and allocation rate across 5 payload +-- sizes (~10 B, ~100 B, ~1 KB, ~10 KB, ~100 KB) for both codegen modes +-- (full inline / runtime descriptor). Emits a JSON document on stdout that +-- can be compared against `bench/baseline.json`. +-- +-- Usage: +-- tarantool bench/bench.lua -- run, print JSON +-- tarantool bench/bench.lua --baseline -- overwrite baseline.json +-- tarantool bench/bench.lua --compare -- compare vs baseline.json +-- exit nonzero if any +-- throughput regresses >5% + +package.path = './runtime/?.lua;./runtime/?/init.lua;' + .. './examples/expected/?.lua;./examples/expected/?/init.lua;' + .. package.path + +local clock = require('clock') +local json = require('json') +local fio = require('fio') + +local MODES = {'full', 'runtime'} +local SIZES = { + {label = '10B', target = 10}, + {label = '100B', target = 100}, + {label = '1KB', target = 1024}, + {label = '10KB', target = 10240}, + {label = '100KB', target = 102400}, +} + +-- Build a `Person` payload whose encoded size is close to `target` bytes. +-- +-- Strategy: pick one knob per decade so each size still exercises the +-- full encoder (varints, packed repeated, length-delimited strings, +-- nested messages) — not just one giant byte-string. +local function build_payload(target) + if target <= 10 then + -- name(6) + age(1) ⇒ 10 bytes encoded. + return {name = 'bigbes', age = 42} + end + if target <= 100 then + -- name (~target-10 bytes string) gives a tight fit (~94 B). + return { + name = string.rep('a', target - 10), + age = 42, + } + end + -- For >=1 KB: scale `emails` (length-delimited strings) and add nested + -- + packed repeated fields so the shape stays representative. + local per_email = 36 -- tag(1) + len(1) + 32 bytes content + slack + local fixed_bytes = 80 -- name + age + address + lucky_numbers + overhead + local n_emails = math.max(1, math.floor((target - fixed_bytes) / per_email)) + local p = { + name = 'bigbes', + age = 42, + address = {street = '1 Main St', city = 'Springfield', zip = 12345}, + lucky_numbers = {7, 13, 21, 42, 99}, + emails = {}, + } + for i = 1, n_emails do + p.emails[i] = string.rep('e', 28) .. string.format('%04d', i) + end + return p +end + +-- Pick iteration count adaptively: smaller messages need more iters to +-- amortize loop + clock overhead; larger messages need fewer to keep +-- wall time bounded. +local function iter_count(size_bytes) + if size_bytes < 100 then return 200000 end + if size_bytes < 2000 then return 50000 end + if size_bytes < 20000 then return 5000 end + return 500 +end + +-- Median + min/max from a small sample. Median rejects single-trace +-- compilation outliers; min is closer to steady-state JIT performance. +local function summarize(samples) + table.sort(samples) + local n = #samples + local median = samples[math.floor((n + 1) / 2)] + return { + median = median, + min = samples[1], + max = samples[n], + } +end + +local function time_loop(fn, n) + local t0 = clock.monotonic64() + for _ = 1, n do fn() end + local t1 = clock.monotonic64() + return tonumber(t1 - t0) / 1e9 -- seconds +end + +local function bench_throughput(fn, n, runs) + -- Warmup: let the JIT compile. + for _ = 1, math.min(n, 1000) do fn() end + local times = {} + for r = 1, runs do + collectgarbage('collect') + times[r] = time_loop(fn, n) + end + local s = summarize(times) + return { + ns_per_op = s.median / n * 1e9, + msgs_per_s = n / s.median, + runs = runs, + iters = n, + time_min_s = s.min, + time_med_s = s.median, + time_max_s = s.max, + } +end + +-- Allocation per op. Stop GC, run a small batch, measure delta in KB. +-- Restart GC immediately so the next bench isn't polluted. +-- +-- Iteration count is capped so peak retained memory stays under ~64 MB +-- — for 100KB messages 1000 iters would hold 500MB live and trigger OS +-- swap pressure that skews adjacent throughput readings. +local function bench_alloc(fn, expected_bytes) + local budget = 64 * 1024 * 1024 + local per_iter = math.max(1, expected_bytes) * 2 + local n = math.max(100, math.min(2000, math.floor(budget / per_iter))) + -- prime: ensure any one-shot allocations (descriptor lookups, jit + -- traces) already happened. + for _ = 1, 100 do fn() end + collectgarbage('collect') + collectgarbage('stop') + local before = collectgarbage('count') + for _ = 1, n do fn() end + local after = collectgarbage('count') + collectgarbage('restart') + collectgarbage('collect') + return { + kb_per_op = (after - before) / n, + bytes_per_op = (after - before) * 1024 / n, + iters = n, + } +end + +local function bench_one(mode, size) + local hello_pb = require(mode .. '.hello.hello_pb') + local encode = hello_pb.Person_encode + local decode = hello_pb.Person_decode + + local payload = build_payload(size.target) + local bytes = encode(payload) + local n = iter_count(#bytes) + local runs = 5 + + -- Re-decode once so warmup hot path matches. + local _ = decode(bytes) + + local enc_throughput = bench_throughput(function() encode(payload) end, n, runs) + local enc_alloc = bench_alloc(function() encode(payload) end, #bytes) + enc_throughput.mb_per_s = #bytes * enc_throughput.msgs_per_s / 1e6 + enc_throughput.alloc_kb_per_op = enc_alloc.kb_per_op + enc_throughput.alloc_bytes_per_op = enc_alloc.bytes_per_op + + local dec_throughput = bench_throughput(function() decode(bytes) end, n, runs) + local dec_alloc = bench_alloc(function() decode(bytes) end, #bytes) + dec_throughput.mb_per_s = #bytes * dec_throughput.msgs_per_s / 1e6 + dec_throughput.alloc_kb_per_op = dec_alloc.kb_per_op + dec_throughput.alloc_bytes_per_op = dec_alloc.bytes_per_op + + return { + mode = mode, + size_label = size.label, + size_bytes = #bytes, + encode = enc_throughput, + decode = dec_throughput, + } +end + +local function run_all() + local results = {} + for _, mode in ipairs(MODES) do + for _, size in ipairs(SIZES) do + io.stderr:write(string.format(' bench %s/%s ... ', mode, size.label)) + io.stderr:flush() + local r = bench_one(mode, size) + io.stderr:write(string.format( + 'enc %.0f msgs/s (%.1f MB/s) dec %.0f msgs/s (%.1f MB/s)\n', + r.encode.msgs_per_s, r.encode.mb_per_s, + r.decode.msgs_per_s, r.decode.mb_per_s)) + results[#results + 1] = r + end + end + return { + tarantool = _TARANTOOL, + jit = jit and jit.version or nil, + schema_message = 'hello.Person', + results = results, + } +end + +-- Render JSON deterministically: arrays preserve order, but Lua tables +-- iterate in hash order. We control key emission for each level. +local function render_metric(t) + return string.format( + '{"ns_per_op": %.1f, "msgs_per_s": %.0f, "mb_per_s": %.3f, ' + .. '"alloc_kb_per_op": %.3f, "alloc_bytes_per_op": %.1f, ' + .. '"iters": %d, "runs": %d, "time_med_s": %.6f, ' + .. '"time_min_s": %.6f, "time_max_s": %.6f}', + t.ns_per_op, t.msgs_per_s, t.mb_per_s, + t.alloc_kb_per_op, t.alloc_bytes_per_op, + t.iters, t.runs, t.time_med_s, t.time_min_s, t.time_max_s) +end + +local function render(doc) + local lines = {} + lines[#lines + 1] = '{' + lines[#lines + 1] = string.format(' "tarantool": %s,', json.encode(doc.tarantool)) + lines[#lines + 1] = string.format(' "jit": %s,', json.encode(doc.jit or json.NULL)) + lines[#lines + 1] = string.format(' "schema_message": %s,', json.encode(doc.schema_message)) + lines[#lines + 1] = ' "results": [' + for i, r in ipairs(doc.results) do + local sep = (i == #doc.results) and '' or ',' + lines[#lines + 1] = ' {' + lines[#lines + 1] = string.format(' "mode": %s,', json.encode(r.mode)) + lines[#lines + 1] = string.format(' "size_label": %s,', json.encode(r.size_label)) + lines[#lines + 1] = string.format(' "size_bytes": %d,', r.size_bytes) + lines[#lines + 1] = string.format(' "encode": %s,', render_metric(r.encode)) + lines[#lines + 1] = string.format(' "decode": %s', render_metric(r.decode)) + lines[#lines + 1] = ' }' .. sep + end + lines[#lines + 1] = ' ]' + lines[#lines + 1] = '}' + return table.concat(lines, '\n') .. '\n' +end + +-- Hardware-portable baseline: throughput (msgs/s, MB/s) varies with CPU +-- load and is unsuitable for committed baselines. Allocation per op is +-- reproducible to within ~10 bytes regardless of machine — it counts +-- bytes, not time — so that's all we commit. Throughput is in --print +-- output for human inspection only. +local function reduce_for_baseline(doc) + local by_key = {} + for _, r in ipairs(doc.results) do + by_key[r.mode .. '/' .. r.size_label] = r + end + local out = {} + for _, size in ipairs(SIZES) do + local full = by_key['full/' .. size.label] + local runtime = by_key['runtime/' .. size.label] + out[#out + 1] = { + size_label = size.label, + size_bytes = full.size_bytes, + encode = { + alloc_kb_per_op_full = full.encode.alloc_kb_per_op, + alloc_kb_per_op_runtime = runtime.encode.alloc_kb_per_op, + }, + decode = { + alloc_kb_per_op_full = full.decode.alloc_kb_per_op, + alloc_kb_per_op_runtime = runtime.decode.alloc_kb_per_op, + }, + } + end + return {schema_message = doc.schema_message, results = out} +end + +local function render_baseline(reduced) + local lines = {'{'} + lines[#lines + 1] = string.format(' "schema_message": %s,', json.encode(reduced.schema_message)) + lines[#lines + 1] = ' "results": [' + for i, r in ipairs(reduced.results) do + local sep = (i == #reduced.results) and '' or ',' + lines[#lines + 1] = ' {' + lines[#lines + 1] = string.format(' "size_label": %s,', json.encode(r.size_label)) + lines[#lines + 1] = string.format(' "size_bytes": %d,', r.size_bytes) + lines[#lines + 1] = string.format( + ' "encode": {"alloc_kb_per_op_full": %.3f, ' + .. '"alloc_kb_per_op_runtime": %.3f},', + r.encode.alloc_kb_per_op_full, + r.encode.alloc_kb_per_op_runtime) + lines[#lines + 1] = string.format( + ' "decode": {"alloc_kb_per_op_full": %.3f, ' + .. '"alloc_kb_per_op_runtime": %.3f}', + r.decode.alloc_kb_per_op_full, + r.decode.alloc_kb_per_op_runtime) + lines[#lines + 1] = ' }' .. sep + end + lines[#lines + 1] = ' ]' + lines[#lines + 1] = '}' + return table.concat(lines, '\n') .. '\n' +end + +-- Compare two reduced baselines, return list of regressions exceeding +-- `tolerance` (fraction, e.g. 0.05 = 5%). +-- +-- Allocation per op is the regression gate. It's hardware-independent +-- (counts bytes, not time), reproducible to within ~10 bytes per op, +-- and a direct measure of encoder/decoder efficiency. Throughput +-- ratios swing 30%+ run-to-run on a busy laptop — useless as a gate. +local function compare(current, baseline, tolerance) + local function index(b) + local m = {} + for _, r in ipairs(b.results) do m[r.size_label] = r end + return m + end + local cur = index(current) + local base = index(baseline) + local regressions = {} + for _, size in ipairs(SIZES) do + local c = cur[size.label] + local b = base[size.label] + if not c or not b then goto continue end + for _, op in ipairs({'encode', 'decode'}) do + for _, key in ipairs({'alloc_kb_per_op_full', 'alloc_kb_per_op_runtime'}) do + local bv, cv = b[op][key], c[op][key] + if bv > 0 and cv > bv * (1 + tolerance) then + regressions[#regressions + 1] = string.format( + '%s/%s %s: %.3f -> %.3f KB/op (+%.1f%%)', + size.label, op, key, bv, cv, (cv / bv - 1) * 100) + end + end + end + ::continue:: + end + return regressions +end + +local args = {...} +local mode_flag = args[1] or '--print' + +io.stderr:write(string.format('tarantool-protobuf bench (%s)\n', _TARANTOOL)) +local doc = run_all() + +if mode_flag == '--print' then + io.write(render(doc)) +elseif mode_flag == '--baseline' then + local out = render_baseline(reduce_for_baseline(doc)) + local path = 'bench/baseline.json' + local f = assert(fio.open(path, {'O_WRONLY', 'O_CREAT', 'O_TRUNC'}, tonumber('644', 8))) + f:write(out) + f:close() + io.stderr:write(string.format('wrote %s\n', path)) + io.write(out) +elseif mode_flag == '--compare' then + local path = 'bench/baseline.json' + local f = assert(fio.open(path, {'O_RDONLY'})) + local baseline = json.decode(f:read()) + f:close() + local current = reduce_for_baseline(doc) + local regs = compare(current, baseline, 0.05) + if #regs == 0 then + io.stderr:write('no regressions >5% vs baseline\n') + os.exit(0) + end + io.stderr:write(string.format('REGRESSIONS vs baseline (>5%%):\n')) + for _, r in ipairs(regs) do io.stderr:write(' ' .. r .. '\n') end + os.exit(1) +else + io.stderr:write('usage: bench.lua [--print | --baseline | --compare]\n') + os.exit(2) +end + +os.exit(0) diff --git a/cmd/conformance-runner.lua b/cmd/conformance-runner.lua new file mode 100644 index 0000000000000000000000000000000000000000..bffa492bafb58291076e3cfe2e6cb96f5411b1df --- /dev/null +++ b/cmd/conformance-runner.lua @@ -0,0 +1,54 @@ +#!/usr/bin/env tarantool +-- +-- protoc-gen-tarantool / pb runtime conformance test runner. +-- +-- Speaks the Google protobuf conformance protocol on stdin/stdout: each +-- request and response is a little-endian uint32 length followed by a +-- `conformance.ConformanceRequest` / `conformance.ConformanceResponse` +-- serialized as protobuf. Loops until EOF. +-- +-- Run against the canonical Google conformance binary like so: +-- +-- conformance_test_runner --enforce_recommended \ +-- tarantool cmd/conformance-runner.lua +-- +-- LUA_PATH must let this script find `pb`, the generated modules, and +-- the conformance core module under `cmd/`: +-- +-- LUA_PATH="./runtime/?/init.lua;./runtime/?.lua;\ +-- ./examples/expected/?.lua;./examples/expected/?/init.lua;\ +-- ./cmd/?.lua;;" + +local core = require('cmd.conformance.core') + +local function read_n(n) + local got = io.stdin:read(n) + if got == nil or #got < n then return nil end + return got +end + +local function read_request_bytes() + local hdr = read_n(4) + if hdr == nil then return nil end + local b1, b2, b3, b4 = hdr:byte(1, 4) + return read_n(b1 + b2 * 256 + b3 * 65536 + b4 * 16777216) +end + +local function write_response_bytes(payload) + local n = #payload + io.stdout:write(string.char( + n % 256, + math.floor(n / 256) % 256, + math.floor(n / 65536) % 256, + math.floor(n / 16777216) % 256)) + io.stdout:write(payload) + io.stdout:flush() +end + +while true do + local req_bytes = read_request_bytes() + if req_bytes == nil then break end + write_response_bytes(core.handle_request(req_bytes)) +end + +os.exit(0) diff --git a/cmd/conformance/core.lua b/cmd/conformance/core.lua new file mode 100644 index 0000000000000000000000000000000000000000..e5c400cd36906bf502b71dc8af0d35cccc78054e --- /dev/null +++ b/cmd/conformance/core.lua @@ -0,0 +1,107 @@ +-- Core dispatch for the Google protobuf conformance protocol. +-- +-- Decodes a `conformance.ConformanceRequest` (wire bytes), runs it through +-- our codec/JSON in the requested format, and returns the encoded +-- `conformance.ConformanceResponse` bytes. The stdin/stdout framing is +-- handled by the thin wrapper in `cmd/conformance-runner.lua`. +-- +-- Split out as a module so the luatest suite can drive `handle_request` +-- directly without spawning a subprocess. + +local pb = require('pb') +local conformance = require('full.conformance.conformance_pb') +local proto3_tests = require('full.protobuf_test_messages.proto3.test_messages_proto3_pb') + +local M = {} + +-- Map of supported `message_type` -> descriptor. Any other message type +-- yields a `skipped` response so we don't claim conformance for protos we +-- don't actually support yet (proto2, editions). +local MESSAGE_REGISTRY = { + ['protobuf_test_messages.proto3.TestAllTypesProto3'] = + proto3_tests.TestAllTypesProto3_descriptor, + ['conformance.FailureSet'] = + conformance.FailureSet_descriptor, +} + +local WIRE_FORMAT = conformance.WireFormat +local PROTOBUF = WIRE_FORMAT.PROTOBUF +local JSON = WIRE_FORMAT.JSON +local JSPB = WIRE_FORMAT.JSPB +local TEXT_FORMAT = WIRE_FORMAT.TEXT_FORMAT + +local function dispatch(req) + local desc = MESSAGE_REGISTRY[req.message_type] + if desc == nil then + return {skipped = 'unsupported message type: ' .. + tostring(req.message_type)} + end + + -- 1. Decode the input payload into a Lua table. + local msg + if req.protobuf_payload ~= nil then + local ok, decoded = pcall(pb.decode, desc, req.protobuf_payload) + if not ok then + return {parse_error = 'protobuf decode failed: ' .. + tostring(decoded)} + end + msg = decoded + elseif req.json_payload ~= nil then + local ok, decoded = pcall(pb.json.decode, desc, req.json_payload) + if not ok then + return {parse_error = 'json decode failed: ' .. tostring(decoded)} + end + msg = decoded + elseif req.jspb_payload ~= nil or req.text_payload ~= nil then + return {skipped = 'jspb/text input not supported'} + else + return {runtime_error = 'no payload set in ConformanceRequest'} + end + + -- 2. Serialize in the requested output format. + local out_fmt = req.requested_output_format + if out_fmt == PROTOBUF then + local ok, bytes = pcall(pb.encode, desc, msg) + if not ok then + return {serialize_error = 'protobuf encode failed: ' .. + tostring(bytes)} + end + return {protobuf_payload = bytes} + elseif out_fmt == JSON then + local ok, jbytes = pcall(pb.json.encode, desc, msg) + if not ok then + return {serialize_error = 'json encode failed: ' .. + tostring(jbytes)} + end + return {json_payload = jbytes} + elseif out_fmt == JSPB or out_fmt == TEXT_FORMAT then + return {skipped = 'jspb/text output not supported'} + else + return {runtime_error = 'unknown requested_output_format: ' .. + tostring(out_fmt)} + end +end + +-- Takes the raw bytes of a ConformanceRequest, returns the raw bytes of +-- a ConformanceResponse. Never throws — every failure path produces a +-- well-formed ConformanceResponse so the conformance runner stays synced. +function M.handle_request(req_bytes) + local ok, req = pcall(conformance.ConformanceRequest_decode, req_bytes) + if not ok then + return conformance.ConformanceResponse_encode( + {runtime_error = 'failed to decode ConformanceRequest: ' .. + tostring(req)}) + end + local resp = dispatch(req) + local ok2, bytes = pcall(conformance.ConformanceResponse_encode, resp) + if not ok2 then + return conformance.ConformanceResponse_encode( + {runtime_error = 'failed to encode response: ' .. tostring(bytes)}) + end + return bytes +end + +-- Expose for introspection / extension. +M.MESSAGE_REGISTRY = MESSAGE_REGISTRY + +return M diff --git a/cmd/protoc-gen-tarantool/internal/gen/gen.go b/cmd/protoc-gen-tarantool/internal/gen/gen.go new file mode 100644 index 0000000000000000000000000000000000000000..89e7120e1ae1e18519d65d91b2fc736d7b7b561f --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/gen.go @@ -0,0 +1,437 @@ +// Package gen contains the per-file Lua codegen used by protoc-gen-tarantool. +package gen + +import ( + "fmt" + "sort" + "strconv" + "strings" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +const runtimeRequire = "pb" + +// Mode controls how generated _encode / _decode wrappers are produced. +// +// - ModeFull (default): emit per-message inline encode/decode functions +// that call wire primitives directly, no descriptor dispatch. Faster, +// more JIT-friendly, larger output. +// - ModeRuntime: emit thin wrappers that delegate to pb.encode / pb.decode +// against the (always-emitted) descriptor table. Slower, smaller output, +// useful for introspection. +// +// The descriptor table is emitted in both modes so users can introspect +// schemas and so future tooling (registry, dynamic types) keeps working. +type Mode int + +const ( + ModeFull Mode = iota + ModeRuntime +) + +// ParseMode converts a CLI value (full|runtime) to a Mode. Empty -> default. +func ParseMode(s string) (Mode, error) { + switch s { + case "", "full": + return ModeFull, nil + case "runtime": + return ModeRuntime, nil + } + return ModeFull, fmt.Errorf("unknown mode %q (want full|runtime)", s) +} + +// Config carries per-invocation generator options. +type Config struct { + Mode Mode + // Prefix, when non-empty, is prepended to every generated module's Lua + // require path (and its on-disk subpath). Lets the same .proto be + // generated under multiple namespaces in one project — e.g. for + // side-by-side full vs runtime mode comparison in tests. + Prefix string +} + +// GenerateFile emits one `.lua` file per input `.proto`. +func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error { + if file.Desc.Syntax() != protoreflect.Proto3 { + return fmt.Errorf("%s: only proto3 is supported, got %s", + file.Desc.Path(), file.Desc.Syntax()) + } + + allMsgs := flattenMessagesSkippingMapEntries(file.Messages, nil) + allEnums := flattenEnums(file.Enums, file.Messages) + + out := plug.NewGeneratedFile(outputFilename(file.Desc, cfg.Prefix), "") + w := &writer{GeneratedFile: out} + + emitHeader(w, file) + imports := collectImports(file, allMsgs, cfg.Prefix) + emitRequires(w, imports) + + w.line("local M = {}") + w.line("") + + // 1) Enums first (no forward-ref problems). + for _, e := range allEnums { + emitEnum(w, file, e) + } + + // 2) Predeclare all message descriptor tables (so cross-references resolve). + if len(allMsgs) > 0 { + w.line("-- Pre-declare message descriptors so cross-references resolve.") + for _, m := range allMsgs { + name := luaTypeName(m.Desc.FullName(), file.Desc.Package()) + w.line("M.%s_descriptor = {name = %q}", name, string(m.Desc.FullName())) + } + w.line("") + } + + // 3) Fill in fields[] for each message and finalize. + for _, m := range allMsgs { + emitMessageFields(w, file, m, imports, cfg.Prefix) + } + + // 4) Wrappers: _new / _encode / _decode. + for _, m := range allMsgs { + switch cfg.Mode { + case ModeFull: + emitInlineMessage(w, file, m, imports, cfg.Prefix) + default: + emitMessageWrappers(w, file, m) + } + } + + // 5) Services (mode-independent — client/server stubs delegate to the + // per-message _encode/_decode functions emitted in step 4). + for _, svc := range file.Services { + emitService(w, file, svc, imports, cfg.Prefix) + } + + w.line("return M") + return nil +} + +// ---------------------------------------------------------------------------- +// writer: thin wrapper for line-oriented emission. +// ---------------------------------------------------------------------------- + +type writer struct { + *protogen.GeneratedFile +} + +func (w *writer) line(format string, args ...any) { + if len(args) == 0 { + w.P(format) + } else { + w.P(fmt.Sprintf(format, args...)) + } +} + +// ---------------------------------------------------------------------------- +// Header & requires +// ---------------------------------------------------------------------------- + +func emitHeader(w *writer, file *protogen.File) { + w.line("-- Code generated by protoc-gen-tarantool. DO NOT EDIT.") + w.line("-- source: %s", file.Desc.Path()) + w.line("-- syntax: %s", file.Desc.Syntax()) + if pkg := string(file.Desc.Package()); pkg != "" { + w.line("-- package: %s", pkg) + } + w.line("") + w.line("local pb = require(%q)", runtimeRequire) + w.line("local wire = pb.wire") +} + +// collectImports returns the deduplicated set of Lua require paths for all +// other .proto files referenced by message fields *and* service inputs/outputs +// in this file. +func collectImports(file *protogen.File, msgs []*protogen.Message, prefix string) map[string]string { + selfPath := luaPackagePath(file.Desc, prefix) + out := map[string]string{} + addType := func(ext protoreflect.FileDescriptor) { + if ext == nil { + return + } + if isWellKnownTypeFile(ext) { + return // pb.wkt is reachable via the existing `pb` require + } + lp := luaPackagePath(ext, prefix) + if lp == selfPath { + return + } + out[lp] = importAlias(lp) + } + for _, m := range msgs { + for _, f := range m.Fields { + switch { + case f.Message != nil: + addType(f.Message.Desc.ParentFile()) + case f.Enum != nil: + addType(f.Enum.Desc.ParentFile()) + } + } + } + for _, svc := range file.Services { + for _, meth := range svc.Methods { + addType(meth.Input.Desc.ParentFile()) + addType(meth.Output.Desc.ParentFile()) + } + } + return out +} + +func emitRequires(w *writer, imports map[string]string) { + if len(imports) == 0 { + w.line("") + return + } + keys := make([]string, 0, len(imports)) + for k := range imports { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + w.line("local %s = require(%q)", imports[k], k) + } + w.line("") +} + +// ---------------------------------------------------------------------------- +// Enum emission +// ---------------------------------------------------------------------------- + +func emitEnum(w *writer, file *protogen.File, e *protogen.Enum) { + name := luaTypeName(e.Desc.FullName(), file.Desc.Package()) + w.line("-- Enum: %s", e.Desc.FullName()) + w.line("M.%s_descriptor = pb.enum(%q, {", name, string(e.Desc.FullName())) + for _, v := range e.Values { + w.line(" %s = %d,", string(v.Desc.Name()), v.Desc.Number()) + } + w.line("})") + // Convenience aliases the user can reach via `M.MyEnum.RED`, etc. + w.line("M.%s = M.%s_descriptor.by_name", name, name) + w.line("") +} + +// ---------------------------------------------------------------------------- +// Message emission +// ---------------------------------------------------------------------------- + +// emitMessageFields fills the predeclared M._descriptor with its +// fields[] array and finalizes it (which builds field_by_id). +func emitMessageFields(w *writer, file *protogen.File, m *protogen.Message, imports map[string]string, prefix string) { + name := luaTypeName(m.Desc.FullName(), file.Desc.Package()) + selfPath := luaPackagePath(file.Desc, prefix) + + w.line("-- Message: %s", m.Desc.FullName()) + w.line("M.%s_descriptor.fields = {", name) + for _, f := range m.Fields { + w.line(" %s,", renderFieldEntry(file, f, selfPath, imports, prefix)) + } + w.line("}") + emitOneofTable(w, name, m) + w.line("pb.finalize_message(M.%s_descriptor)", name) + w.line("") +} + +// emitOneofTable emits `M._descriptor.oneofs = { = {...members...} }` +// when the message has any non-synthetic oneofs. +func emitOneofTable(w *writer, name string, m *protogen.Message) { + type oneofRow struct { + name string + members []string + } + var rows []oneofRow + for _, oo := range m.Oneofs { + // Skip synthetic oneofs created for proto3 explicit `optional` — those + // have a single member that uses HasOptionalKeyword(). + if oo.Fields[0].Desc.HasOptionalKeyword() { + continue + } + row := oneofRow{name: string(oo.Desc.Name())} + for _, f := range oo.Fields { + row.members = append(row.members, string(f.Desc.Name())) + } + rows = append(rows, row) + } + if len(rows) == 0 { + return + } + w.line("M.%s_descriptor.oneofs = {", name) + for _, row := range rows { + quoted := make([]string, 0, len(row.members)) + for _, fn := range row.members { + quoted = append(quoted, fmt.Sprintf("%q", fn)) + } + w.line(" %s = {%s},", row.name, strings.Join(quoted, ", ")) + } + w.line("}") +} + +// renderFieldEntry produces the Lua table literal for a single field descriptor. +func renderFieldEntry(file *protogen.File, f *protogen.Field, selfPath string, imports map[string]string, prefix string) string { + parts := []string{ + fmt.Sprintf("name=%q", string(f.Desc.Name())), + fmt.Sprintf("id=%d", f.Desc.Number()), + } + + if f.Desc.IsMap() { + parts = append(parts, "kind='map'") + parts = append(parts, "key="+renderMapEntry(file, f.Message.Fields[0], selfPath, imports, prefix)) + parts = append(parts, "value="+renderMapEntry(file, f.Message.Fields[1], selfPath, imports, prefix)) + return "{" + strings.Join(parts, ", ") + "}" + } + + switch { + case f.Message != nil: + parts = append(parts, "kind='message'") + parts = append(parts, "message="+typeRef(file, f.Message.Desc, selfPath, imports, "_descriptor", prefix)) + case f.Enum != nil: + parts = append(parts, "kind='enum'") + parts = append(parts, "enum="+typeRef(file, f.Enum.Desc, selfPath, imports, "_descriptor", prefix)) + default: + s := scalarName(f.Desc.Kind()) + if s == "" { + panic("unhandled scalar kind: " + f.Desc.Kind().String()) + } + parts = append(parts, "kind='scalar'") + parts = append(parts, "proto_type="+strconv.Quote(s)) + } + + if f.Desc.IsList() { + parts = append(parts, "repeated=true") + // proto3 packed default for primitives + enums is true; explicit + // `[packed=false]` flips it. IsPacked() returns the effective value. + if f.Message == nil && f.Desc.Kind() != protoreflect.StringKind && + f.Desc.Kind() != protoreflect.BytesKind { + if f.Desc.IsPacked() { + parts = append(parts, "packed=true") + } else { + parts = append(parts, "packed=false") + } + } + } + + // Oneof membership. (Skip synthetic oneofs that proto3 explicit `optional` + // expands into — those are surfaced as `optional=true` instead.) + if f.Oneof != nil && !f.Desc.HasOptionalKeyword() { + parts = append(parts, fmt.Sprintf("oneof=%q", string(f.Oneof.Desc.Name()))) + } + + // Proto3 explicit optional (field presence). + if f.Desc.HasOptionalKeyword() { + parts = append(parts, "optional=true") + } + + return "{" + strings.Join(parts, ", ") + "}" +} + +// renderMapEntry renders a sub-field descriptor for a map's key or value. +// It mirrors renderFieldEntry but always for a singular non-map value, and +// emits without the `name`/`id` (caller knows: id 1 = key, id 2 = value). +func renderMapEntry(file *protogen.File, f *protogen.Field, selfPath string, imports map[string]string, prefix string) string { + parts := []string{} + switch { + case f.Message != nil: + parts = append(parts, "kind='message'") + parts = append(parts, "message="+typeRef(file, f.Message.Desc, selfPath, imports, "_descriptor", prefix)) + case f.Enum != nil: + parts = append(parts, "kind='enum'") + parts = append(parts, "enum="+typeRef(file, f.Enum.Desc, selfPath, imports, "_descriptor", prefix)) + default: + s := scalarName(f.Desc.Kind()) + if s == "" { + panic("unhandled map sub-field kind: " + f.Desc.Kind().String()) + } + parts = append(parts, "kind='scalar'") + parts = append(parts, "proto_type="+strconv.Quote(s)) + } + return "{" + strings.Join(parts, ", ") + "}" +} + +// typeRef returns a Lua expression evaluating to the descriptor of the given +// type (a Message or Enum), resolving cross-file imports as needed. +func typeRef(file *protogen.File, td protoreflect.Descriptor, selfPath string, imports map[string]string, suffix string, prefix string) string { + parent := td.ParentFile() + if isWellKnownTypeFile(parent) { + return "pb.wkt." + wktTypeName(td.FullName()) + suffix + } + luaName := luaTypeName(td.FullName(), parent.Package()) + parentPath := luaPackagePath(parent, prefix) + if parentPath == selfPath { + return "M." + luaName + suffix + } + alias, ok := imports[parentPath] + if !ok { + // Should be impossible if collectImports walked all fields. + alias = importAlias(parentPath) + } + return alias + "." + luaName + suffix +} + +// emitMessageWrappers emits the small _new / _encode / _decode helpers plus +// has_ / clear_ for each explicit-optional field. +func emitMessageWrappers(w *writer, file *protogen.File, m *protogen.Message) { + name := luaTypeName(m.Desc.FullName(), file.Desc.Package()) + w.line("function M.%s_new(t) return t or {} end", name) + w.line("function M.%s_encode(t) return pb.encode(M.%s_descriptor, t) end", name, name) + w.line("function M.%s_decode(b) return pb.decode(M.%s_descriptor, b) end", name, name) + emitOptionalAccessors(w, name, m) + w.line("") +} + +// emitOptionalAccessors writes M._has_(t) and _clear_(t) +// for every field marked with proto3 explicit `optional`. +func emitOptionalAccessors(w *writer, name string, m *protogen.Message) { + for _, f := range m.Fields { + if !f.Desc.HasOptionalKeyword() { + continue + } + fname := string(f.Desc.Name()) + w.line("function M.%s_has_%s(t) return t.%s ~= nil end", name, fname, fname) + w.line("function M.%s_clear_%s(t) t.%s = nil end", name, fname, fname) + } +} + +// ---------------------------------------------------------------------------- +// Flattening helpers +// ---------------------------------------------------------------------------- + +// flattenMessages returns top-level + all nested messages in declaration order. +func flattenMessages(top []*protogen.Message, acc []*protogen.Message) []*protogen.Message { + for _, m := range top { + acc = append(acc, m) + acc = flattenMessages(m.Messages, acc) + } + return acc +} + +// flattenMessagesSkippingMapEntries is like flattenMessages but excludes the +// synthetic Entry messages protoc generates for `map` fields. +// Those don't get their own Lua descriptor — map handling is inline. +func flattenMessagesSkippingMapEntries(top []*protogen.Message, acc []*protogen.Message) []*protogen.Message { + for _, m := range top { + if m.Desc.IsMapEntry() { + continue + } + acc = append(acc, m) + acc = flattenMessagesSkippingMapEntries(m.Messages, acc) + } + return acc +} + +// flattenEnums returns top-level enums + all enums nested inside messages. +func flattenEnums(topEnums []*protogen.Enum, msgs []*protogen.Message) []*protogen.Enum { + out := append([]*protogen.Enum{}, topEnums...) + var walk func(ms []*protogen.Message) + walk = func(ms []*protogen.Message) { + for _, m := range ms { + out = append(out, m.Enums...) + walk(m.Messages) + } + } + walk(msgs) + return out +} diff --git a/cmd/protoc-gen-tarantool/internal/gen/inline.go b/cmd/protoc-gen-tarantool/internal/gen/inline.go new file mode 100644 index 0000000000000000000000000000000000000000..bb5536e1c0b65983d493b840837b2d4c507fea67 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/inline.go @@ -0,0 +1,603 @@ +package gen + +import ( + "fmt" + "strings" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// emitInlineMessage emits per-message _new / _encode / _decode functions with +// no descriptor dispatch. Tag bytes are precomputed as Lua string literals; +// each scalar field's encode/decode call resolves to one wire. call. +func emitInlineMessage(w *writer, file *protogen.File, m *protogen.Message, imports map[string]string, prefix string) { + name := luaTypeName(m.Desc.FullName(), file.Desc.Package()) + selfPath := luaPackagePath(file.Desc, prefix) + + w.line("function M.%s_new(t) return t or {} end", name) + w.line("") + + emitInlineEncode(w, name, m, file, selfPath, imports, prefix) + emitInlineDecode(w, name, m, file, selfPath, imports, prefix) + emitOptionalAccessors(w, name, m) + w.line("") +} + +func emitInlineEncode(w *writer, name string, m *protogen.Message, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + w.line("function M.%s_encode(t)", name) + w.line(" if type(t) ~= 'table' then") + w.line(" error(\"expected table for %s, got \" .. type(t), 0)", m.Desc.FullName()) + w.line(" end") + w.line(" local out, n = {}, 0") + w.line(" local v") + + // Oneof pre-pass: pick the active branch per oneof (last-set wins). + for _, oo := range realOneofs(m) { + w.line(" local %s", oneofVar(string(oo.Desc.Name()))) + for _, f := range oo.Fields { + w.line(" if t.%s ~= nil then %s = %q end", + string(f.Desc.Name()), + oneofVar(string(oo.Desc.Name())), + string(f.Desc.Name())) + } + } + + for _, f := range m.Fields { + emitInlineEncodeField(w, f, file, selfPath, imports, prefix) + } + // Preserve unknown fields captured at decode time. + w.line(" local _uf = t._unknown_fields") + w.line(" if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end") + w.line(" return table.concat(out)") + w.line("end") + w.line("") +} + +// realOneofs returns the message's non-synthetic oneofs (skips the ones +// proto3 expands explicit `optional` into). +func realOneofs(m *protogen.Message) []*protogen.Oneof { + var out []*protogen.Oneof + for _, oo := range m.Oneofs { + if oo.Fields[0].Desc.HasOptionalKeyword() { + continue + } + out = append(out, oo) + } + return out +} + +func oneofVar(name string) string { return "_of_" + name } + +// fieldRealOneof returns the oneof name a field belongs to, or "" if the +// field is not in a real oneof (i.e. either standalone or in a synthetic +// proto3 explicit-optional oneof). +func fieldRealOneof(f *protogen.Field) string { + if f.Oneof == nil || f.Desc.HasOptionalKeyword() { + return "" + } + return string(f.Oneof.Desc.Name()) +} + +func emitInlineEncodeField(w *writer, f *protogen.Field, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + id := int32(f.Desc.Number()) + tag := tagBytesLit(id, wireTypeForField(f)) + fname := string(f.Desc.Name()) + + w.line(" -- field %d: %s", id, fname) + w.line(" v = t.%s", fname) + + oneof := fieldRealOneof(f) + gate := "v ~= nil" + if oneof != "" { + gate = fmt.Sprintf("%s == %q", oneofVar(oneof), fname) + } + // Explicit-optional fields: presence is meaningful, no default elision. + hasPresence := oneof != "" || f.Desc.HasOptionalKeyword() + + switch { + case f.Desc.IsMap(): + emitInlineEncodeMap(w, f, tag, file, selfPath, imports, prefix) + case f.Desc.IsList(): + emitInlineEncodeRepeated(w, f, tag, fname, file, selfPath, imports, prefix) + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_encode", prefix) + w.line(" if %s then", gate) + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_len(%s(v))", ref) + w.line(" end") + case f.Enum != nil: + enumLocal := typeRef(file, f.Enum.Desc, selfPath, imports, "", prefix) + fullName := string(f.Enum.Desc.FullName()) + // Presence (oneof or optional): emit even when value is the enum default. + w.line(" if %s then", gate) + w.line(" local nv = v") + w.line(" if type(v) == 'string' then") + w.line(" nv = %s[v]", enumLocal) + w.line(" if nv == nil then error(\"unknown enum value '\" .. v .. \"' for %s\", 0) end", fullName) + w.line(" end") + if !hasPresence { + w.line(" if nv ~= 0 then") + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_int32(nv)") + w.line(" end") + } else { + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_int32(nv)") + } + w.line(" end") + default: + st := scalarName(f.Desc.Kind()) + if st == "" { + panic("unhandled scalar kind: " + f.Desc.Kind().String()) + } + if !hasPresence { + w.line(" if v ~= nil and %s then", scalarNotDefaultExpr(st, "v")) + } else { + w.line(" if %s then", gate) + } + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_%s(v)", st) + w.line(" end") + } +} + +func emitInlineEncodeRepeated(w *writer, f *protogen.Field, tag, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + switch { + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_encode", prefix) + w.line(" if v ~= nil and #v > 0 then") + w.line(" local _tag = %s", tag) + w.line(" for _i = 1, #v do") + w.line(" n = n + 1; out[n] = _tag") + w.line(" n = n + 1; out[n] = wire.encode_len(%s(v[_i]))", ref) + w.line(" end") + w.line(" end") + case f.Enum != nil: + enumLocal := typeRef(file, f.Enum.Desc, selfPath, imports, "", prefix) + fullName := string(f.Enum.Desc.FullName()) + w.line(" if v ~= nil and #v > 0 then") + w.line(" local parts, m = {}, 0") + w.line(" for _i = 1, #v do") + w.line(" local elem = v[_i]") + w.line(" local nv = elem") + w.line(" if type(elem) == 'string' then") + w.line(" nv = %s[elem]", enumLocal) + w.line(" if nv == nil then error(\"unknown enum value '\" .. elem .. \"' for %s\", 0) end", fullName) + w.line(" end") + w.line(" m = m + 1; parts[m] = wire.encode_int32(nv)") + w.line(" end") + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_len(table.concat(parts))") + w.line(" end") + default: + st := scalarName(f.Desc.Kind()) + packable := st != "string" && st != "bytes" + if packable && f.Desc.IsPacked() { + w.line(" if v ~= nil and #v > 0 then") + w.line(" local parts, m = {}, 0") + w.line(" for _i = 1, #v do") + w.line(" m = m + 1; parts[m] = wire.encode_%s(v[_i])", st) + w.line(" end") + w.line(" n = n + 1; out[n] = %s", tag) + w.line(" n = n + 1; out[n] = wire.encode_len(table.concat(parts))") + w.line(" end") + } else { + w.line(" if v ~= nil and #v > 0 then") + w.line(" local _tag = %s", tag) + w.line(" for _i = 1, #v do") + w.line(" n = n + 1; out[n] = _tag") + w.line(" n = n + 1; out[n] = wire.encode_%s(v[_i])", st) + w.line(" end") + w.line(" end") + } + } +} + +func emitInlineDecode(w *writer, name string, m *protogen.Message, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + w.line("function M.%s_decode(buf)", name) + w.line(" if type(buf) ~= 'string' then") + w.line(" error(\"expected string for %s decode, got \" .. type(buf), 0)", m.Desc.FullName()) + w.line(" end") + w.line(" local result = {}") + w.line(" local pos, len = 1, #buf") + w.line(" local _uf") + w.line(" while pos <= len do") + w.line(" local _tag_start = pos") + w.line(" local id, wt") + w.line(" id, wt, pos = wire.decode_tag(buf, pos)") + + first := true + for _, f := range m.Fields { + op := "elseif" + if first { + op = "if" + first = false + } + w.line(" %s id == %d then", op, f.Desc.Number()) + emitInlineDecodeFieldBody(w, f, file, selfPath, imports, prefix) + } + if first { + // No fields — every wire byte is unknown. + w.line(" if true then") + } + w.line(" else") + w.line(" pos = wire.skip_field(buf, pos, wt)") + w.line(" if _uf == nil then _uf = {} end") + w.line(" _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1)") + w.line(" end") + w.line(" end") + w.line(" if _uf ~= nil then result._unknown_fields = table.concat(_uf) end") + w.line(" return result") + w.line("end") + w.line("") +} + +func emitInlineDecodeFieldBody(w *writer, f *protogen.Field, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + fname := string(f.Desc.Name()) + switch { + case f.Desc.IsMap(): + emitInlineDecodeMap(w, f, fname, file, selfPath, imports, prefix) + case f.Desc.IsList(): + emitInlineDecodeRepeated(w, f, fname, file, selfPath, imports, prefix) + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_decode", prefix) + oneof := fieldRealOneof(f) + w.line(" local payload") + w.line(" payload, pos = wire.decode_len(buf, pos)") + if oneof != "" { + // Oneof branches are exclusive; replace, don't merge. + w.line(" result.%s = %s(payload)", fname, ref) + } else { + w.line(" local prev = result.%s", fname) + w.line(" if prev == nil then") + w.line(" result.%s = %s(payload)", fname, ref) + w.line(" else") + w.line(" local new = %s(payload)", ref) + w.line(" for k, val in pairs(new) do prev[k] = val end") + w.line(" end") + } + case f.Enum != nil: + w.line(" local u") + w.line(" u, pos = wire.decode_varint(buf, pos)") + w.line(" result.%s = tonumber(u)", fname) + default: + st := scalarName(f.Desc.Kind()) + w.line(" local val") + w.line(" val, pos = wire.decode_%s(buf, pos)", st) + w.line(" result.%s = val", fname) + } + + // Oneof: clear sibling branches so callers see exactly one set field. + if oneof := fieldRealOneof(f); oneof != "" { + for _, sib := range f.Oneof.Fields { + if sib == f { + continue + } + w.line(" result.%s = nil", string(sib.Desc.Name())) + } + } +} + +func emitInlineDecodeRepeated(w *writer, f *protogen.Field, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + w.line(" local list = result.%s", fname) + w.line(" if list == nil then list = {}; result.%s = list end", fname) + + switch { + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_decode", prefix) + w.line(" local payload") + w.line(" payload, pos = wire.decode_len(buf, pos)") + w.line(" list[#list + 1] = %s(payload)", ref) + case f.Enum != nil: + // Enums are packable (proto3 default). Accept both packed and per-element. + w.line(" if wt == 2 then") + w.line(" local payload") + w.line(" payload, pos = wire.decode_len(buf, pos)") + w.line(" local p2, lim = 1, #payload") + w.line(" while p2 <= lim do") + w.line(" local u") + w.line(" u, p2 = wire.decode_varint(payload, p2)") + w.line(" list[#list + 1] = tonumber(u)") + w.line(" end") + w.line(" else") + w.line(" local u") + w.line(" u, pos = wire.decode_varint(buf, pos)") + w.line(" list[#list + 1] = tonumber(u)") + w.line(" end") + default: + st := scalarName(f.Desc.Kind()) + packable := st != "string" && st != "bytes" + if packable { + w.line(" if wt == 2 then") + w.line(" local payload") + w.line(" payload, pos = wire.decode_len(buf, pos)") + w.line(" local p2, lim = 1, #payload") + w.line(" while p2 <= lim do") + w.line(" local val") + w.line(" val, p2 = wire.decode_%s(payload, p2)", st) + w.line(" list[#list + 1] = val") + w.line(" end") + w.line(" else") + w.line(" local val") + w.line(" val, pos = wire.decode_%s(buf, pos)", st) + w.line(" list[#list + 1] = val") + w.line(" end") + } else { + w.line(" local val") + w.line(" val, pos = wire.decode_%s(buf, pos)", st) + w.line(" list[#list + 1] = val") + } + } +} + +// ---------------------------------------------------------------------------- +// Map field codegen +// ---------------------------------------------------------------------------- + +// emitInlineEncodeMap emits the encode block for a map field. Map fields +// are wire-equivalent to `repeated Entry`, where the synthetic Entry +// message has key=field 1 and value=field 2. +func emitInlineEncodeMap(w *writer, f *protogen.Field, tag string, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + keyF, valF := f.Message.Fields[0], f.Message.Fields[1] + + keyTag := tagBytesLit(1, mapSubFieldWireType(keyF)) + valTag := tagBytesLit(2, mapSubFieldWireType(valF)) + + w.line(" if v ~= nil and next(v) ~= nil then") + w.line(" local _tag, _ktag, _vtag = %s, %s, %s", tag, keyTag, valTag) + w.line(" for _k, _val in pairs(v) do") + w.line(" local entry, _m = {}, 0") + + // Key emit + keyDef := mapKeyDefaultExpr(keyF) + w.line(" if _k ~= %s then", keyDef) + emitMapPiece(w, "entry", "_m", "_ktag", "_k", keyF, file, selfPath, imports, prefix) + w.line(" end") + + // Value emit + emitMapValueGuard(w, valF, "_val") + emitMapPiece(w, "entry", "_m", "_vtag", "_val", valF, file, selfPath, imports, prefix) + w.line(" end") + + w.line(" n = n + 1; out[n] = _tag") + w.line(" n = n + 1; out[n] = wire.encode_len(table.concat(entry))") + w.line(" end") + w.line(" end") +} + +// emitMapPiece emits the two-line append: +// +// [ + 1] = ; [ + 2] = wire.encode_() +// +// (or the message form). Increments by 2 in two separate statements. +func emitMapPiece(w *writer, list, idx, tag, valExpr string, f *protogen.Field, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + switch { + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_encode", prefix) + w.line(" %s = %s + 1; %s[%s] = %s", + idx, idx, list, idx, tag) + w.line(" %s = %s + 1; %s[%s] = wire.encode_len(%s(%s))", + idx, idx, list, idx, ref, valExpr) + case f.Enum != nil: + // For enum value: input may be string name; resolve. + enumLocal := typeRef(file, f.Enum.Desc, selfPath, imports, "", prefix) + fullName := string(f.Enum.Desc.FullName()) + w.line(" local _nv = %s", valExpr) + w.line(" if type(%s) == 'string' then", valExpr) + w.line(" _nv = %s[%s]", enumLocal, valExpr) + w.line(" if _nv == nil then error(\"unknown enum value '\" .. %s .. \"' for %s\", 0) end", + valExpr, fullName) + w.line(" end") + w.line(" %s = %s + 1; %s[%s] = %s", idx, idx, list, idx, tag) + w.line(" %s = %s + 1; %s[%s] = wire.encode_int32(_nv)", idx, idx, list, idx) + default: + st := scalarName(f.Desc.Kind()) + w.line(" %s = %s + 1; %s[%s] = %s", idx, idx, list, idx, tag) + w.line(" %s = %s + 1; %s[%s] = wire.encode_%s(%s)", + idx, idx, list, idx, st, valExpr) + } +} + +// emitMapValueGuard emits an `if then` guarding the value emit +// for default-elision in map entries. Closing `end` is the caller's job. +func emitMapValueGuard(w *writer, f *protogen.Field, valExpr string) { + switch { + case f.Message != nil: + // Messages have no notion of "default value" elision in this position; + // emit unconditionally (nil is excluded by the outer iteration anyway). + w.line(" if %s ~= nil then", valExpr) + case f.Enum != nil: + // Need to resolve string -> int first, but we delay that. The cheap + // guard here is only for default elision, so check `~= 0` once it's + // been resolved. To keep things simple, always emit and let the proto + // receiver re-resolve to default. (Defaults round-trip correctly.) + w.line(" do") + default: + st := scalarName(f.Desc.Kind()) + w.line(" if %s then", scalarNotDefaultExpr(st, valExpr)) + } +} + +// mapKeyDefaultExpr returns the Lua literal for the proto3 default of a map key. +// Only string + integer + bool keys are valid in maps. +func mapKeyDefaultExpr(f *protogen.Field) string { + switch f.Desc.Kind() { + case protoreflect.StringKind: + return "''" + case protoreflect.BoolKind: + return "false" + } + return "0" +} + +// mapSubFieldWireType returns the wire type for a map entry's key or value +// (both are singular and never packed). +func mapSubFieldWireType(f *protogen.Field) int { + switch { + case f.Message != nil: + return 2 // LEN + case f.Enum != nil: + return 0 // VARINT + } + switch f.Desc.Kind() { + case protoreflect.Int32Kind, protoreflect.Int64Kind, + protoreflect.Uint32Kind, protoreflect.Uint64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind, + protoreflect.BoolKind: + return 0 // VARINT + case protoreflect.Fixed32Kind, protoreflect.Sfixed32Kind, protoreflect.FloatKind: + return 5 // I32 + case protoreflect.Fixed64Kind, protoreflect.Sfixed64Kind, protoreflect.DoubleKind: + return 1 // I64 + case protoreflect.StringKind, protoreflect.BytesKind: + return 2 // LEN + } + panic("unhandled map sub-field kind: " + f.Desc.Kind().String()) +} + +// emitInlineDecodeMap emits the decode block for a map field. +func emitInlineDecodeMap(w *writer, f *protogen.Field, fname string, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + keyF, valF := f.Message.Fields[0], f.Message.Fields[1] + + w.line(" local map = result.%s", fname) + w.line(" if map == nil then map = {}; result.%s = map end", fname) + w.line(" local payload") + w.line(" payload, pos = wire.decode_len(buf, pos)") + w.line(" local _ep, _elim = 1, #payload") + w.line(" local _key, _val = %s, %s", + mapDefaultExpr(keyF), mapDefaultExpr(valF)) + w.line(" while _ep <= _elim do") + w.line(" local eid, ewt") + w.line(" eid, ewt, _ep = wire.decode_tag(payload, _ep)") + w.line(" if eid == 1 then") + emitMapDecode(w, "_key", keyF, file, selfPath, imports, prefix) + w.line(" elseif eid == 2 then") + emitMapDecode(w, "_val", valF, file, selfPath, imports, prefix) + w.line(" else") + w.line(" _ep = wire.skip_field(payload, _ep, ewt)") + w.line(" end") + w.line(" end") + w.line(" map[_key] = _val") +} + +// mapDefaultExpr returns the Lua expression for a map sub-field's default. +func mapDefaultExpr(f *protogen.Field) string { + switch { + case f.Message != nil: + return "{}" + case f.Enum != nil: + return "0" + } + switch f.Desc.Kind() { + case protoreflect.StringKind, protoreflect.BytesKind: + return "''" + case protoreflect.BoolKind: + return "false" + } + return "0" +} + +// emitMapDecode emits the per-sub-field decode body inside the map entry's +// while-loop. Stores into ; advances _ep. +func emitMapDecode(w *writer, dst string, f *protogen.Field, file *protogen.File, selfPath string, imports map[string]string, prefix string) { + switch { + case f.Message != nil: + ref := typeRef(file, f.Message.Desc, selfPath, imports, "_decode", prefix) + w.line(" local _payload") + w.line(" _payload, _ep = wire.decode_len(payload, _ep)") + w.line(" %s = %s(_payload)", dst, ref) + case f.Enum != nil: + w.line(" local _u") + w.line(" _u, _ep = wire.decode_varint(payload, _ep)") + w.line(" %s = tonumber(_u)", dst) + default: + st := scalarName(f.Desc.Kind()) + w.line(" %s, _ep = wire.decode_%s(payload, _ep)", dst, st) + } +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// wireTypeForField returns the proto wire type used to encode this field. +// +// For repeated packable fields with packed=true (proto3 default for primitive +// scalars and enums), the *element* tag is LEN — caller still calls this with +// care. We return the singular-element wire type and let the emit logic decide +// when to swap it to LEN for packed encoding. +func wireTypeForField(f *protogen.Field) int { + switch { + case f.Message != nil: + return 2 // LEN + case f.Enum != nil: + // Repeated packed enums use LEN tag; non-packed elements use VARINT. + if f.Desc.IsList() && f.Desc.IsPacked() { + return 2 + } + return 0 // VARINT + } + switch f.Desc.Kind() { + case protoreflect.Int32Kind, protoreflect.Int64Kind, + protoreflect.Uint32Kind, protoreflect.Uint64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind, + protoreflect.BoolKind: + // Repeated packed primitives use LEN tag. + if f.Desc.IsList() && f.Desc.IsPacked() { + return 2 + } + return 0 // VARINT + case protoreflect.Fixed32Kind, protoreflect.Sfixed32Kind, protoreflect.FloatKind: + if f.Desc.IsList() && f.Desc.IsPacked() { + return 2 + } + return 5 // I32 + case protoreflect.Fixed64Kind, protoreflect.Sfixed64Kind, protoreflect.DoubleKind: + if f.Desc.IsList() && f.Desc.IsPacked() { + return 2 + } + return 1 // I64 + case protoreflect.StringKind, protoreflect.BytesKind: + return 2 // LEN (never packable) + } + panic("unhandled kind: " + f.Desc.Kind().String()) +} + +// tagBytesLit returns a Lua string literal (e.g. "\"\\x0a\"") encoding the +// varint for tag = (id << 3) | wireType. Tags are 1 byte for ids ≤ 15 with +// VARINT/I32/I64, 1 byte for ids ≤ 31 with LEN, 2 bytes for ids ≤ 2047, +// and so on. +func tagBytesLit(id int32, wireType int) string { + tag := uint64(id)*8 + uint64(wireType) + var b []byte + for tag >= 128 { + b = append(b, byte(tag&0x7f)|0x80) + tag >>= 7 + } + b = append(b, byte(tag)) + return luaByteString(b) +} + +func luaByteString(b []byte) string { + var sb strings.Builder + sb.WriteByte('"') + for _, c := range b { + sb.WriteString(fmt.Sprintf("\\x%02x", c)) + } + sb.WriteByte('"') + return sb.String() +} + +// scalarNotDefaultExpr returns a Lua expression that evaluates true when +// the value `v` is NOT the proto3 default for the given scalar type. +// Default is elided on encode. +func scalarNotDefaultExpr(scalar, v string) string { + switch scalar { + case "string", "bytes": + return v + ` ~= ''` + case "bool": + return v + ` ~= false` + } + return v + ` ~= 0` +} diff --git a/cmd/protoc-gen-tarantool/internal/gen/name.go b/cmd/protoc-gen-tarantool/internal/gen/name.go new file mode 100644 index 0000000000000000000000000000000000000000..85be2f2245b538cc336c4697dbfbecb673f598b2 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/name.go @@ -0,0 +1,82 @@ +package gen + +import ( + "path" + "strings" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +// luaPackagePath returns the dotted Lua require path for a generated file. +// +// Resolution order: +// 1. file option (tarantool.lua_package), used as-is +// 2. proto package + file basename + "_pb" +// e.g. package=foo.bar, file=baz.proto -> "foo.bar.baz_pb" +// 3. file basename only (when proto package is empty) +// e.g. file=baz.proto -> "baz_pb" +func luaPackagePath(f protoreflect.FileDescriptor, prefix string) string { + var p string + if v := luaPackageOption(f); v != "" { + p = v + } else { + base := strings.TrimSuffix(path.Base(f.Path()), ".proto") + "_pb" + if pkg := string(f.Package()); pkg != "" { + p = pkg + "." + base + } else { + p = base + } + } + if prefix != "" { + return prefix + "." + p + } + return p +} + +// outputFilename returns the on-disk path (relative to the protoc out dir) +// for a generated file. Mirrors the dotted Lua require path with `/`-separators +// and a `.lua` extension. +// +// For example, lua package "myapp.proto.foo" -> "myapp/proto/foo.lua". +func outputFilename(f protoreflect.FileDescriptor, prefix string) string { + return strings.ReplaceAll(luaPackagePath(f, prefix), ".", "/") + ".lua" +} + +// luaTypeName returns the underscore-flattened type name used in generated +// Lua module tables. Strips the leading proto-package prefix. +// +// Examples (assuming file package = "foo.bar"): +// foo.bar.Person -> "Person" +// foo.bar.Outer.Inner -> "Outer_Inner" +// foo.bar.Color -> "Color" +func luaTypeName(fullName protoreflect.FullName, filePkg protoreflect.FullName) string { + s := string(fullName) + if filePkg != "" { + prefix := string(filePkg) + "." + s = strings.TrimPrefix(s, prefix) + } + return strings.ReplaceAll(s, ".", "_") +} + +// importAlias produces a stable Lua local-variable name for a required module. +// It dot-separates the module path and joins with underscores, prefixed with +// "_imp_" to avoid clashing with user identifiers. +// +// "myapp.proto.foo" -> "_imp_myapp_proto_foo" +func importAlias(luaPath string) string { + return "_imp_" + strings.ReplaceAll(luaPath, ".", "_") +} + +// isWellKnownTypeFile reports whether the file declares Google's +// google.protobuf.* well-known types. Those are fulfilled by `pb.wkt` +// at runtime and don't need a separate generated module. +func isWellKnownTypeFile(fd protoreflect.FileDescriptor) bool { + return string(fd.Package()) == "google.protobuf" +} + +// wktTypeName strips the `google.protobuf.` prefix from a WKT type's full +// name. Example: "google.protobuf.Timestamp" -> "Timestamp". +func wktTypeName(full protoreflect.FullName) string { + return strings.TrimPrefix(string(full), "google.protobuf.") +} + diff --git a/cmd/protoc-gen-tarantool/internal/gen/options.go b/cmd/protoc-gen-tarantool/internal/gen/options.go new file mode 100644 index 0000000000000000000000000000000000000000..e1ff83a634f6e394639e77d65ba25e9d540810a4 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/options.go @@ -0,0 +1,34 @@ +package gen + +import ( + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/runtime/protoimpl" + "google.golang.org/protobuf/types/descriptorpb" +) + +// E_LuaPackage mirrors the option declared in options/tarantool/tarantool.proto. +// We register it manually here to avoid having to ship a generated stub for our +// own option file. +var E_LuaPackage = &protoimpl.ExtensionInfo{ + ExtendedType: (*descriptorpb.FileOptions)(nil), + ExtensionType: (*string)(nil), + Field: 60001, + Name: "tarantool.lua_package", + Tag: "bytes,60001,opt,name=lua_package", + Filename: "tarantool/tarantool.proto", +} + +// luaPackageOption returns the value of (tarantool.lua_package) on the file +// options, or "" when unset. +func luaPackageOption(f protoreflect.FileDescriptor) string { + opts, _ := f.Options().(*descriptorpb.FileOptions) + if opts == nil { + return "" + } + v, ok := proto.GetExtension(opts, E_LuaPackage).(string) + if !ok { + return "" + } + return v +} diff --git a/cmd/protoc-gen-tarantool/internal/gen/service.go b/cmd/protoc-gen-tarantool/internal/gen/service.go new file mode 100644 index 0000000000000000000000000000000000000000..90e7a8c9d067a4659439eb6737b393c3a1becd09 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/service.go @@ -0,0 +1,202 @@ +package gen + +import ( + "google.golang.org/protobuf/compiler/protogen" +) + +// streamKind classifies an RPC method for codegen branching. +type streamKind int + +const ( + kindUnary streamKind = iota + kindServerStream + kindClientStream + kindBidi +) + +func classify(m *protogen.Method) streamKind { + cs, ss := m.Desc.IsStreamingClient(), m.Desc.IsStreamingServer() + switch { + case cs && ss: + return kindBidi + case ss: + return kindServerStream + case cs: + return kindClientStream + default: + return kindUnary + } +} + +// emitService emits the descriptor + client + server factory for a single +// gRPC service. The descriptor is mode-independent; client/server factories +// reference the same per-message _encode/_decode functions that the rest of +// the module already provides. +func emitService(w *writer, file *protogen.File, svc *protogen.Service, imports map[string]string, prefix string) { + name := string(svc.Desc.Name()) + fullName := string(svc.Desc.FullName()) + selfPath := luaPackagePath(file.Desc, prefix) + + w.line("-- Service: %s", fullName) + w.line("M.%s_service = {", name) + w.line(" name = %q,", fullName) + w.line(" full_name = %q,", "/"+fullName) + w.line(" methods = {") + for _, m := range svc.Methods { + mname := string(m.Desc.Name()) + w.line(" %s = {", mname) + w.line(" name = %q,", mname) + w.line(" full_name = %q,", "/"+fullName+"/"+mname) + w.line(" input = %s,", typeRef(file, m.Input.Desc, selfPath, imports, "_descriptor", prefix)) + w.line(" output = %s,", typeRef(file, m.Output.Desc, selfPath, imports, "_descriptor", prefix)) + if m.Desc.IsStreamingClient() { + w.line(" client_streaming = true,") + } + if m.Desc.IsStreamingServer() { + w.line(" server_streaming = true,") + } + w.line(" },") + } + w.line(" },") + w.line("}") + w.line("") + + emitServiceClient(w, file, svc, imports, prefix, selfPath) + emitServiceServer(w, file, svc, imports, prefix, selfPath) +} + +// emitServiceClient emits a constructor `function M._client(transport)` +// that returns a table with one entry per RPC method: +// +// - Unary methods are direct functions: `client.SayHello(req, ctx) -> reply` +// - Streaming methods return a stream object (see pb.grpc for the shape): +// `client.StreamHellos(req, ctx) -> {recv, cancel}` +// `client.CollectHellos(ctx) -> {send, close_send, recv, cancel}` +// `client.Chat(ctx) -> {send, close_send, recv, cancel}` +func emitServiceClient(w *writer, file *protogen.File, svc *protogen.Service, imports map[string]string, prefix string, selfPath string) { + name := string(svc.Desc.Name()) + w.line("function M.%s_client(transport)", name) + w.line(" if transport == nil then error(\"%s_client: transport is required\", 0) end", name) + w.line(" return {") + for _, m := range svc.Methods { + mname := string(m.Desc.Name()) + path := "/" + string(svc.Desc.FullName()) + "/" + mname + inputEnc := typeRef(file, m.Input.Desc, selfPath, imports, "_encode", prefix) + outputDec := typeRef(file, m.Output.Desc, selfPath, imports, "_decode", prefix) + + switch classify(m) { + case kindUnary: + w.line(" %s = function(req, ctx)", mname) + w.line(" local req_bytes = %s(req)", inputEnc) + w.line(" local resp_bytes = transport:unary(%q, req_bytes, ctx)", path) + w.line(" return %s(resp_bytes)", outputDec) + w.line(" end,") + case kindServerStream: + w.line(" %s = function(req, ctx)", mname) + w.line(" local req_bytes = %s(req)", inputEnc) + w.line(" local raw = transport:server_stream(%q, req_bytes, ctx)", path) + w.line(" return pb.grpc.wrap_server_stream(raw, %s)", outputDec) + w.line(" end,") + case kindClientStream: + w.line(" %s = function(ctx)", mname) + w.line(" local raw = transport:client_stream(%q, ctx)", path) + w.line(" return pb.grpc.wrap_call(raw, %s, %s)", inputEnc, outputDec) + w.line(" end,") + case kindBidi: + w.line(" %s = function(ctx)", mname) + w.line(" local raw = transport:bidi(%q, ctx)", path) + w.line(" return pb.grpc.wrap_call(raw, %s, %s)", inputEnc, outputDec) + w.line(" end,") + } + } + w.line(" }") + w.line("end") + w.line("") +} + +// emitServiceServer emits `function M._server(impl)` returning +// {service, methods, streams}. `methods` holds unary handlers keyed by +// path; `streams` holds streaming handlers keyed by path. Each streaming +// entry is `{kind = '...', handler = function(req_bytes, server_view, ctx)}` +// — see pb.grpc for the transport's expectations. +// +// User-supplied impl functions speak decoded messages; the generated +// wrappers handle the per-message encode/decode boundary so user code +// stays free of wire details. +func emitServiceServer(w *writer, file *protogen.File, svc *protogen.Service, imports map[string]string, prefix string, selfPath string) { + name := string(svc.Desc.Name()) + w.line("function M.%s_server(impl)", name) + w.line(" if type(impl) ~= 'table' then error(\"%s_server: impl table is required\", 0) end", name) + w.line(" return {") + w.line(" service = M.%s_service,", name) + w.line(" methods = {") + for _, m := range svc.Methods { + if classify(m) != kindUnary { + continue + } + mname := string(m.Desc.Name()) + path := "/" + string(svc.Desc.FullName()) + "/" + mname + inputDec := typeRef(file, m.Input.Desc, selfPath, imports, "_decode", prefix) + outputEnc := typeRef(file, m.Output.Desc, selfPath, imports, "_encode", prefix) + + w.line(" [%q] = function(req_bytes, ctx)", path) + w.line(" local handler = impl.%s", mname) + w.line(" if handler == nil then error(\"%s.%s: handler missing\", 0) end", name, mname) + w.line(" local req = %s(req_bytes)", inputDec) + w.line(" local resp = handler(req, ctx)") + w.line(" return %s(resp)", outputEnc) + w.line(" end,") + } + w.line(" },") + w.line(" streams = {") + for _, m := range svc.Methods { + kind := classify(m) + if kind == kindUnary { + continue + } + mname := string(m.Desc.Name()) + path := "/" + string(svc.Desc.FullName()) + "/" + mname + inputDec := typeRef(file, m.Input.Desc, selfPath, imports, "_decode", prefix) + outputEnc := typeRef(file, m.Output.Desc, selfPath, imports, "_encode", prefix) + + switch kind { + case kindServerStream: + w.line(" [%q] = {", path) + w.line(" kind = 'server_stream',") + w.line(" handler = function(req_bytes, server_view, ctx)") + w.line(" local handler = impl.%s", mname) + w.line(" if handler == nil then error(\"%s.%s: handler missing\", 0) end", name, mname) + w.line(" local req = %s(req_bytes)", inputDec) + w.line(" local wrapped = pb.grpc.wrap_server_view(server_view, nil, %s)", outputEnc) + w.line(" handler(req, wrapped, ctx)") + w.line(" end,") + w.line(" },") + case kindClientStream: + w.line(" [%q] = {", path) + w.line(" kind = 'client_stream',") + w.line(" handler = function(_, server_view, ctx)") + w.line(" local handler = impl.%s", mname) + w.line(" if handler == nil then error(\"%s.%s: handler missing\", 0) end", name, mname) + w.line(" local wrapped = pb.grpc.wrap_server_view(server_view, %s, nil)", inputDec) + w.line(" local resp = handler(wrapped, ctx)") + w.line(" if resp == nil then error(\"%s.%s: handler returned nil response\", 0) end", name, mname) + w.line(" server_view:send(%s(resp))", outputEnc) + w.line(" end,") + w.line(" },") + case kindBidi: + w.line(" [%q] = {", path) + w.line(" kind = 'bidi',") + w.line(" handler = function(_, server_view, ctx)") + w.line(" local handler = impl.%s", mname) + w.line(" if handler == nil then error(\"%s.%s: handler missing\", 0) end", name, mname) + w.line(" local wrapped = pb.grpc.wrap_server_view(server_view, %s, %s)", inputDec, outputEnc) + w.line(" handler(wrapped, ctx)") + w.line(" end,") + w.line(" },") + } + } + w.line(" },") + w.line(" }") + w.line("end") + w.line("") +} diff --git a/cmd/protoc-gen-tarantool/internal/gen/types.go b/cmd/protoc-gen-tarantool/internal/gen/types.go new file mode 100644 index 0000000000000000000000000000000000000000..b61fdbd1016be288bd99515a4dc63bcb0005f773 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/types.go @@ -0,0 +1,44 @@ +package gen + +import "google.golang.org/protobuf/reflect/protoreflect" + +// scalarName converts a proto Kind to the string the Lua runtime expects in +// field descriptors (matches the keys of `pb.scalar` in runtime/protobuf/pb.lua). +// +// Returns "" for non-scalar kinds (Message, Group, Enum) — callers handle +// those separately. +func scalarName(k protoreflect.Kind) string { + switch k { + case protoreflect.BoolKind: + return "bool" + case protoreflect.Int32Kind: + return "int32" + case protoreflect.Sint32Kind: + return "sint32" + case protoreflect.Uint32Kind: + return "uint32" + case protoreflect.Int64Kind: + return "int64" + case protoreflect.Sint64Kind: + return "sint64" + case protoreflect.Uint64Kind: + return "uint64" + case protoreflect.Sfixed32Kind: + return "sfixed32" + case protoreflect.Fixed32Kind: + return "fixed32" + case protoreflect.FloatKind: + return "float" + case protoreflect.Sfixed64Kind: + return "sfixed64" + case protoreflect.Fixed64Kind: + return "fixed64" + case protoreflect.DoubleKind: + return "double" + case protoreflect.StringKind: + return "string" + case protoreflect.BytesKind: + return "bytes" + } + return "" +} diff --git a/cmd/protoc-gen-tarantool/main.go b/cmd/protoc-gen-tarantool/main.go new file mode 100644 index 0000000000000000000000000000000000000000..6362197803de7a82c8e2b0925f9a8ca69268b8cc --- /dev/null +++ b/cmd/protoc-gen-tarantool/main.go @@ -0,0 +1,90 @@ +// protoc-gen-tarantool is a protoc plugin that generates Lua code targeting +// Tarantool's LuaJIT runtime, paired with the runtime/protobuf Lua package. +// +// Usage: +// protoc --tarantool_out=./out --plugin=./protoc-gen-tarantool foo.proto +// +// File option (in your .proto): +// import "tarantool/tarantool.proto"; +// option (tarantool.lua_package) = "myapp.proto.foo"; +package main + +import ( + "flag" + "fmt" + "io" + "os" + "strings" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/pluginpb" + + "sourcecraft.dev/bigbes/tarantool-protobuf/cmd/protoc-gen-tarantool/internal/gen" +) + +func main() { + in, err := io.ReadAll(os.Stdin) + if err != nil { + fail("read stdin: %v", err) + } + req := &pluginpb.CodeGeneratorRequest{} + if err := proto.Unmarshal(in, req); err != nil { + fail("parse CodeGeneratorRequest: %v", err) + } + + // protogen requires a go_package on every input file even when we are not + // generating Go. Inject a synthetic value when it's missing — it's never + // surfaced to the generated Lua. + for _, f := range req.ProtoFile { + if f.Options == nil { + f.Options = &descriptorpb.FileOptions{} + } + if f.Options.GoPackage == nil { + stub := "tarantoolpb_synthetic/" + strings.TrimSuffix(f.GetName(), ".proto") + f.Options.GoPackage = proto.String(stub) + } + } + + var flags flag.FlagSet + modeFlag := flags.String("mode", "full", "codegen mode: full | runtime") + prefixFlag := flags.String("prefix", "", + "prefix prepended to every generated module's Lua require path "+ + "(useful for side-by-side generation in tests)") + plugin, err := protogen.Options{ParamFunc: flags.Set}.New(req) + if err != nil { + fail("init protogen: %v", err) + } + + mode, err := gen.ParseMode(*modeFlag) + if err != nil { + fail("%v", err) + } + cfg := gen.Config{Mode: mode, Prefix: *prefixFlag} + + // Advertise proto3 optional support so protoc lets us see those fields. + plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + + for _, file := range plugin.Files { + if !file.Generate { + continue + } + if err := gen.GenerateFile(plugin, file, cfg); err != nil { + plugin.Error(err) + } + } + + out, err := proto.Marshal(plugin.Response()) + if err != nil { + fail("marshal CodeGeneratorResponse: %v", err) + } + if _, err := os.Stdout.Write(out); err != nil { + fail("write stdout: %v", err) + } +} + +func fail(format string, args ...any) { + fmt.Fprintf(os.Stderr, "protoc-gen-tarantool: "+format+"\n", args...) + os.Exit(1) +} diff --git a/examples/expected/full/conformance/conformance_pb.lua b/examples/expected/full/conformance/conformance_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..1af3f53f3e992e1b159e03339ceae5fa6a34e6b0 --- /dev/null +++ b/examples/expected/full/conformance/conformance_pb.lua @@ -0,0 +1,622 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: conformance.proto +-- syntax: proto3 +-- package: conformance + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: conformance.WireFormat +M.WireFormat_descriptor = pb.enum("conformance.WireFormat", { + UNSPECIFIED = 0, + PROTOBUF = 1, + JSON = 2, + JSPB = 3, + TEXT_FORMAT = 4, +}) +M.WireFormat = M.WireFormat_descriptor.by_name + +-- Enum: conformance.TestCategory +M.TestCategory_descriptor = pb.enum("conformance.TestCategory", { + UNSPECIFIED_TEST = 0, + BINARY_TEST = 1, + JSON_TEST = 2, + JSON_IGNORE_UNKNOWN_PARSING_TEST = 3, + JSPB_TEST = 4, + TEXT_FORMAT_TEST = 5, +}) +M.TestCategory = M.TestCategory_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.TestStatus_descriptor = {name = "conformance.TestStatus"} +M.FailureSet_descriptor = {name = "conformance.FailureSet"} +M.ConformanceRequest_descriptor = {name = "conformance.ConformanceRequest"} +M.ConformanceResponse_descriptor = {name = "conformance.ConformanceResponse"} +M.JspbEncodingConfig_descriptor = {name = "conformance.JspbEncodingConfig"} + +-- Message: conformance.TestStatus +M.TestStatus_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, + {name="failure_message", id=2, kind='scalar', proto_type="string"}, + {name="matched_name", id=3, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.TestStatus_descriptor) + +-- Message: conformance.FailureSet +M.FailureSet_descriptor.fields = { + {name="test", id=2, kind='message', message=M.TestStatus_descriptor, repeated=true}, +} +pb.finalize_message(M.FailureSet_descriptor) + +-- Message: conformance.ConformanceRequest +M.ConformanceRequest_descriptor.fields = { + {name="protobuf_payload", id=1, kind='scalar', proto_type="bytes", oneof="payload"}, + {name="json_payload", id=2, kind='scalar', proto_type="string", oneof="payload"}, + {name="jspb_payload", id=7, kind='scalar', proto_type="string", oneof="payload"}, + {name="text_payload", id=8, kind='scalar', proto_type="string", oneof="payload"}, + {name="requested_output_format", id=3, kind='enum', enum=M.WireFormat_descriptor}, + {name="message_type", id=4, kind='scalar', proto_type="string"}, + {name="test_category", id=5, kind='enum', enum=M.TestCategory_descriptor}, + {name="jspb_encoding_options", id=6, kind='message', message=M.JspbEncodingConfig_descriptor}, + {name="print_unknown_fields", id=9, kind='scalar', proto_type="bool"}, +} +M.ConformanceRequest_descriptor.oneofs = { + payload = {"protobuf_payload", "json_payload", "jspb_payload", "text_payload"}, +} +pb.finalize_message(M.ConformanceRequest_descriptor) + +-- Message: conformance.ConformanceResponse +M.ConformanceResponse_descriptor.fields = { + {name="parse_error", id=1, kind='scalar', proto_type="string", oneof="result"}, + {name="serialize_error", id=6, kind='scalar', proto_type="string", oneof="result"}, + {name="timeout_error", id=9, kind='scalar', proto_type="string", oneof="result"}, + {name="runtime_error", id=2, kind='scalar', proto_type="string", oneof="result"}, + {name="protobuf_payload", id=3, kind='scalar', proto_type="bytes", oneof="result"}, + {name="json_payload", id=4, kind='scalar', proto_type="string", oneof="result"}, + {name="skipped", id=5, kind='scalar', proto_type="string", oneof="result"}, + {name="jspb_payload", id=7, kind='scalar', proto_type="string", oneof="result"}, + {name="text_payload", id=8, kind='scalar', proto_type="string", oneof="result"}, +} +M.ConformanceResponse_descriptor.oneofs = { + result = {"parse_error", "serialize_error", "timeout_error", "runtime_error", "protobuf_payload", "json_payload", "skipped", "jspb_payload", "text_payload"}, +} +pb.finalize_message(M.ConformanceResponse_descriptor) + +-- Message: conformance.JspbEncodingConfig +M.JspbEncodingConfig_descriptor.fields = { + {name="use_jspb_array_any_format", id=1, kind='scalar', proto_type="bool"}, +} +pb.finalize_message(M.JspbEncodingConfig_descriptor) + +function M.TestStatus_new(t) return t or {} end + +function M.TestStatus_encode(t) + if type(t) ~= 'table' then + error("expected table for conformance.TestStatus, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: name + v = t.name + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 2: failure_message + v = t.failure_message + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 3: matched_name + v = t.matched_name + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x1a" + n = n + 1; out[n] = wire.encode_string(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.TestStatus_decode(buf) + if type(buf) ~= 'string' then + error("expected string for conformance.TestStatus decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.name = val + elseif id == 2 then + local val + val, pos = wire.decode_string(buf, pos) + result.failure_message = val + elseif id == 3 then + local val + val, pos = wire.decode_string(buf, pos) + result.matched_name = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.FailureSet_new(t) return t or {} end + +function M.FailureSet_encode(t) + if type(t) ~= 'table' then + error("expected table for conformance.FailureSet, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 2: test + v = t.test + if v ~= nil and #v > 0 then + local _tag = "\x12" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(M.TestStatus_encode(v[_i])) + end + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.FailureSet_decode(buf) + if type(buf) ~= 'string' then + error("expected string for conformance.FailureSet decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 2 then + local list = result.test + if list == nil then list = {}; result.test = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = M.TestStatus_decode(payload) + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.ConformanceRequest_new(t) return t or {} end + +function M.ConformanceRequest_encode(t) + if type(t) ~= 'table' then + error("expected table for conformance.ConformanceRequest, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _of_payload + if t.protobuf_payload ~= nil then _of_payload = "protobuf_payload" end + if t.json_payload ~= nil then _of_payload = "json_payload" end + if t.jspb_payload ~= nil then _of_payload = "jspb_payload" end + if t.text_payload ~= nil then _of_payload = "text_payload" end + -- field 1: protobuf_payload + v = t.protobuf_payload + if _of_payload == "protobuf_payload" then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_bytes(v) + end + -- field 2: json_payload + v = t.json_payload + if _of_payload == "json_payload" then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 7: jspb_payload + v = t.jspb_payload + if _of_payload == "jspb_payload" then + n = n + 1; out[n] = "\x3a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 8: text_payload + v = t.text_payload + if _of_payload == "text_payload" then + n = n + 1; out[n] = "\x42" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 3: requested_output_format + v = t.requested_output_format + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.WireFormat[v] + if nv == nil then error("unknown enum value '" .. v .. "' for conformance.WireFormat", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\x18" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 4: message_type + v = t.message_type + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x22" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 5: test_category + v = t.test_category + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.TestCategory[v] + if nv == nil then error("unknown enum value '" .. v .. "' for conformance.TestCategory", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\x28" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 6: jspb_encoding_options + v = t.jspb_encoding_options + if v ~= nil then + n = n + 1; out[n] = "\x32" + n = n + 1; out[n] = wire.encode_len(M.JspbEncodingConfig_encode(v)) + end + -- field 9: print_unknown_fields + v = t.print_unknown_fields + if v ~= nil and v ~= false then + n = n + 1; out[n] = "\x48" + n = n + 1; out[n] = wire.encode_bool(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.ConformanceRequest_decode(buf) + if type(buf) ~= 'string' then + error("expected string for conformance.ConformanceRequest decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_bytes(buf, pos) + result.protobuf_payload = val + result.json_payload = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 2 then + local val + val, pos = wire.decode_string(buf, pos) + result.json_payload = val + result.protobuf_payload = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 7 then + local val + val, pos = wire.decode_string(buf, pos) + result.jspb_payload = val + result.protobuf_payload = nil + result.json_payload = nil + result.text_payload = nil + elseif id == 8 then + local val + val, pos = wire.decode_string(buf, pos) + result.text_payload = val + result.protobuf_payload = nil + result.json_payload = nil + result.jspb_payload = nil + elseif id == 3 then + local u + u, pos = wire.decode_varint(buf, pos) + result.requested_output_format = tonumber(u) + elseif id == 4 then + local val + val, pos = wire.decode_string(buf, pos) + result.message_type = val + elseif id == 5 then + local u + u, pos = wire.decode_varint(buf, pos) + result.test_category = tonumber(u) + elseif id == 6 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.jspb_encoding_options + if prev == nil then + result.jspb_encoding_options = M.JspbEncodingConfig_decode(payload) + else + local new = M.JspbEncodingConfig_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 9 then + local val + val, pos = wire.decode_bool(buf, pos) + result.print_unknown_fields = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.ConformanceResponse_new(t) return t or {} end + +function M.ConformanceResponse_encode(t) + if type(t) ~= 'table' then + error("expected table for conformance.ConformanceResponse, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _of_result + if t.parse_error ~= nil then _of_result = "parse_error" end + if t.serialize_error ~= nil then _of_result = "serialize_error" end + if t.timeout_error ~= nil then _of_result = "timeout_error" end + if t.runtime_error ~= nil then _of_result = "runtime_error" end + if t.protobuf_payload ~= nil then _of_result = "protobuf_payload" end + if t.json_payload ~= nil then _of_result = "json_payload" end + if t.skipped ~= nil then _of_result = "skipped" end + if t.jspb_payload ~= nil then _of_result = "jspb_payload" end + if t.text_payload ~= nil then _of_result = "text_payload" end + -- field 1: parse_error + v = t.parse_error + if _of_result == "parse_error" then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 6: serialize_error + v = t.serialize_error + if _of_result == "serialize_error" then + n = n + 1; out[n] = "\x32" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 9: timeout_error + v = t.timeout_error + if _of_result == "timeout_error" then + n = n + 1; out[n] = "\x4a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 2: runtime_error + v = t.runtime_error + if _of_result == "runtime_error" then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 3: protobuf_payload + v = t.protobuf_payload + if _of_result == "protobuf_payload" then + n = n + 1; out[n] = "\x1a" + n = n + 1; out[n] = wire.encode_bytes(v) + end + -- field 4: json_payload + v = t.json_payload + if _of_result == "json_payload" then + n = n + 1; out[n] = "\x22" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 5: skipped + v = t.skipped + if _of_result == "skipped" then + n = n + 1; out[n] = "\x2a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 7: jspb_payload + v = t.jspb_payload + if _of_result == "jspb_payload" then + n = n + 1; out[n] = "\x3a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 8: text_payload + v = t.text_payload + if _of_result == "text_payload" then + n = n + 1; out[n] = "\x42" + n = n + 1; out[n] = wire.encode_string(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.ConformanceResponse_decode(buf) + if type(buf) ~= 'string' then + error("expected string for conformance.ConformanceResponse decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.parse_error = val + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 6 then + local val + val, pos = wire.decode_string(buf, pos) + result.serialize_error = val + result.parse_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 9 then + local val + val, pos = wire.decode_string(buf, pos) + result.timeout_error = val + result.parse_error = nil + result.serialize_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 2 then + local val + val, pos = wire.decode_string(buf, pos) + result.runtime_error = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 3 then + local val + val, pos = wire.decode_bytes(buf, pos) + result.protobuf_payload = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 4 then + local val + val, pos = wire.decode_string(buf, pos) + result.json_payload = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.skipped = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 5 then + local val + val, pos = wire.decode_string(buf, pos) + result.skipped = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.jspb_payload = nil + result.text_payload = nil + elseif id == 7 then + local val + val, pos = wire.decode_string(buf, pos) + result.jspb_payload = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.text_payload = nil + elseif id == 8 then + local val + val, pos = wire.decode_string(buf, pos) + result.text_payload = val + result.parse_error = nil + result.serialize_error = nil + result.timeout_error = nil + result.runtime_error = nil + result.protobuf_payload = nil + result.json_payload = nil + result.skipped = nil + result.jspb_payload = nil + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.JspbEncodingConfig_new(t) return t or {} end + +function M.JspbEncodingConfig_encode(t) + if type(t) ~= 'table' then + error("expected table for conformance.JspbEncodingConfig, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: use_jspb_array_any_format + v = t.use_jspb_array_any_format + if v ~= nil and v ~= false then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_bool(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.JspbEncodingConfig_decode(buf) + if type(buf) ~= 'string' then + error("expected string for conformance.JspbEncodingConfig decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_bool(buf, pos) + result.use_jspb_array_any_format = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +return M diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..2060ba40396e0df6d31ffac4e473b8ca293ccf68 --- /dev/null +++ b/examples/expected/full/hello/hello_pb.lua @@ -0,0 +1,993 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: hello.proto +-- syntax: proto3 +-- package: hello + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: hello.Status +M.Status_descriptor = pb.enum("hello.Status", { + UNKNOWN = 0, + OK = 1, + ERROR = 2, +}) +M.Status = M.Status_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.Result_descriptor = {name = "hello.Result"} +M.HelloRequest_descriptor = {name = "hello.HelloRequest"} +M.HelloReply_descriptor = {name = "hello.HelloReply"} +M.Event_descriptor = {name = "hello.Event"} +M.Address_descriptor = {name = "hello.Address"} +M.Person_descriptor = {name = "hello.Person"} + +-- Message: hello.Result +M.Result_descriptor.fields = { + {name="id", id=1, kind='scalar', proto_type="int32"}, + {name="text", id=2, kind='scalar', proto_type="string", oneof="outcome"}, + {name="code", id=3, kind='scalar', proto_type="int32", oneof="outcome"}, + {name="details", id=4, kind='message', message=M.Address_descriptor, oneof="outcome"}, +} +M.Result_descriptor.oneofs = { + outcome = {"text", "code", "details"}, +} +pb.finalize_message(M.Result_descriptor) + +-- Message: hello.HelloRequest +M.HelloRequest_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.HelloRequest_descriptor) + +-- Message: hello.HelloReply +M.HelloReply_descriptor.fields = { + {name="greeting", id=1, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.HelloReply_descriptor) + +-- Message: hello.Event +M.Event_descriptor.fields = { + {name="title", id=1, kind='scalar', proto_type="string"}, + {name="created_at", id=2, kind='message', message=pb.wkt.Timestamp_descriptor}, + {name="duration", id=3, kind='message', message=pb.wkt.Duration_descriptor}, + {name="ack", id=4, kind='message', message=pb.wkt.Empty_descriptor}, + {name="retry_count", id=5, kind='message', message=pb.wkt.Int32Value_descriptor}, + {name="note", id=6, kind='message', message=pb.wkt.StringValue_descriptor}, + {name="is_admin", id=7, kind='message', message=pb.wkt.BoolValue_descriptor}, + {name="payload", id=8, kind='message', message=pb.wkt.Struct_descriptor}, + {name="attribute", id=9, kind='message', message=pb.wkt.Value_descriptor}, + {name="tags", id=10, kind='message', message=pb.wkt.ListValue_descriptor}, + {name="extension", id=11, kind='message', message=pb.wkt.Any_descriptor}, + {name="update_mask", id=12, kind='message', message=pb.wkt.FieldMask_descriptor}, +} +pb.finalize_message(M.Event_descriptor) + +-- Message: hello.Address +M.Address_descriptor.fields = { + {name="street", id=1, kind='scalar', proto_type="string"}, + {name="city", id=2, kind='scalar', proto_type="string"}, + {name="zip", id=3, kind='scalar', proto_type="int32"}, + {name="apartment", id=4, kind='scalar', proto_type="string", optional=true}, +} +pb.finalize_message(M.Address_descriptor) + +-- Message: hello.Person +M.Person_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, + {name="age", id=2, kind='scalar', proto_type="int32"}, + {name="emails", id=3, kind='scalar', proto_type="string", repeated=true}, + {name="status", id=4, kind='enum', enum=M.Status_descriptor}, + {name="address", id=5, kind='message', message=M.Address_descriptor}, + {name="friends", id=6, kind='message', message=M.Person_descriptor, repeated=true}, + {name="lucky_numbers", id=7, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="avatar", id=8, kind='scalar', proto_type="bytes"}, + {name="user_id", id=9, kind='scalar', proto_type="fixed64"}, + {name="balance", id=10, kind='scalar', proto_type="sint32"}, + {name="weight_kg", id=11, kind='scalar', proto_type="double"}, + {name="ages_by_nickname", id=13, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="int32"}}, + {name="nickname_by_age", id=14, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="string"}}, + {name="addresses_by_label", id=15, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.Address_descriptor}}, +} +pb.finalize_message(M.Person_descriptor) + +function M.Result_new(t) return t or {} end + +function M.Result_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.Result, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _of_outcome + if t.text ~= nil then _of_outcome = "text" end + if t.code ~= nil then _of_outcome = "code" end + if t.details ~= nil then _of_outcome = "details" end + -- field 1: id + v = t.id + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 2: text + v = t.text + if _of_outcome == "text" then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 3: code + v = t.code + if _of_outcome == "code" then + n = n + 1; out[n] = "\x18" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 4: details + v = t.details + if _of_outcome == "details" then + n = n + 1; out[n] = "\x22" + n = n + 1; out[n] = wire.encode_len(M.Address_encode(v)) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.Result_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.Result decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_int32(buf, pos) + result.id = val + elseif id == 2 then + local val + val, pos = wire.decode_string(buf, pos) + result.text = val + result.code = nil + result.details = nil + elseif id == 3 then + local val + val, pos = wire.decode_int32(buf, pos) + result.code = val + result.text = nil + result.details = nil + elseif id == 4 then + local payload + payload, pos = wire.decode_len(buf, pos) + result.details = M.Address_decode(payload) + result.text = nil + result.code = nil + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.HelloRequest_new(t) return t or {} end + +function M.HelloRequest_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.HelloRequest, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: name + v = t.name + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.HelloRequest_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.HelloRequest decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.name = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.HelloReply_new(t) return t or {} end + +function M.HelloReply_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.HelloReply, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: greeting + v = t.greeting + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.HelloReply_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.HelloReply decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.greeting = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.Event_new(t) return t or {} end + +function M.Event_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.Event, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: title + v = t.title + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 2: created_at + v = t.created_at + if v ~= nil then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Timestamp_encode(v)) + end + -- field 3: duration + v = t.duration + if v ~= nil then + n = n + 1; out[n] = "\x1a" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Duration_encode(v)) + end + -- field 4: ack + v = t.ack + if v ~= nil then + n = n + 1; out[n] = "\x22" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Empty_encode(v)) + end + -- field 5: retry_count + v = t.retry_count + if v ~= nil then + n = n + 1; out[n] = "\x2a" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Int32Value_encode(v)) + end + -- field 6: note + v = t.note + if v ~= nil then + n = n + 1; out[n] = "\x32" + n = n + 1; out[n] = wire.encode_len(pb.wkt.StringValue_encode(v)) + end + -- field 7: is_admin + v = t.is_admin + if v ~= nil then + n = n + 1; out[n] = "\x3a" + n = n + 1; out[n] = wire.encode_len(pb.wkt.BoolValue_encode(v)) + end + -- field 8: payload + v = t.payload + if v ~= nil then + n = n + 1; out[n] = "\x42" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Struct_encode(v)) + end + -- field 9: attribute + v = t.attribute + if v ~= nil then + n = n + 1; out[n] = "\x4a" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Value_encode(v)) + end + -- field 10: tags + v = t.tags + if v ~= nil then + n = n + 1; out[n] = "\x52" + n = n + 1; out[n] = wire.encode_len(pb.wkt.ListValue_encode(v)) + end + -- field 11: extension + v = t.extension + if v ~= nil then + n = n + 1; out[n] = "\x5a" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Any_encode(v)) + end + -- field 12: update_mask + v = t.update_mask + if v ~= nil then + n = n + 1; out[n] = "\x62" + n = n + 1; out[n] = wire.encode_len(pb.wkt.FieldMask_encode(v)) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.Event_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.Event decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.title = val + elseif id == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.created_at + if prev == nil then + result.created_at = pb.wkt.Timestamp_decode(payload) + else + local new = pb.wkt.Timestamp_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 3 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.duration + if prev == nil then + result.duration = pb.wkt.Duration_decode(payload) + else + local new = pb.wkt.Duration_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 4 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.ack + if prev == nil then + result.ack = pb.wkt.Empty_decode(payload) + else + local new = pb.wkt.Empty_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 5 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.retry_count + if prev == nil then + result.retry_count = pb.wkt.Int32Value_decode(payload) + else + local new = pb.wkt.Int32Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 6 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.note + if prev == nil then + result.note = pb.wkt.StringValue_decode(payload) + else + local new = pb.wkt.StringValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 7 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.is_admin + if prev == nil then + result.is_admin = pb.wkt.BoolValue_decode(payload) + else + local new = pb.wkt.BoolValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 8 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.payload + if prev == nil then + result.payload = pb.wkt.Struct_decode(payload) + else + local new = pb.wkt.Struct_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 9 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.attribute + if prev == nil then + result.attribute = pb.wkt.Value_decode(payload) + else + local new = pb.wkt.Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 10 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.tags + if prev == nil then + result.tags = pb.wkt.ListValue_decode(payload) + else + local new = pb.wkt.ListValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 11 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.extension + if prev == nil then + result.extension = pb.wkt.Any_decode(payload) + else + local new = pb.wkt.Any_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 12 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.update_mask + if prev == nil then + result.update_mask = pb.wkt.FieldMask_decode(payload) + else + local new = pb.wkt.FieldMask_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.Address_new(t) return t or {} end + +function M.Address_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.Address, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: street + v = t.street + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 2: city + v = t.city + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 3: zip + v = t.zip + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x18" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 4: apartment + v = t.apartment + if v ~= nil then + n = n + 1; out[n] = "\x22" + n = n + 1; out[n] = wire.encode_string(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.Address_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.Address decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.street = val + elseif id == 2 then + local val + val, pos = wire.decode_string(buf, pos) + result.city = val + elseif id == 3 then + local val + val, pos = wire.decode_int32(buf, pos) + result.zip = val + elseif id == 4 then + local val + val, pos = wire.decode_string(buf, pos) + result.apartment = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + +function M.Address_has_apartment(t) return t.apartment ~= nil end +function M.Address_clear_apartment(t) t.apartment = nil end + +function M.Person_new(t) return t or {} end + +function M.Person_encode(t) + if type(t) ~= 'table' then + error("expected table for hello.Person, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: name + v = t.name + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x0a" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 2: age + v = t.age + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x10" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 3: emails + v = t.emails + if v ~= nil and #v > 0 then + local _tag = "\x1a" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_string(v[_i]) + end + end + -- field 4: status + v = t.status + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.Status[v] + if nv == nil then error("unknown enum value '" .. v .. "' for hello.Status", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\x20" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 5: address + v = t.address + if v ~= nil then + n = n + 1; out[n] = "\x2a" + n = n + 1; out[n] = wire.encode_len(M.Address_encode(v)) + end + -- field 6: friends + v = t.friends + if v ~= nil and #v > 0 then + local _tag = "\x32" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(M.Person_encode(v[_i])) + end + end + -- field 7: lucky_numbers + v = t.lucky_numbers + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_int32(v[_i]) + end + n = n + 1; out[n] = "\x3a" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 8: avatar + v = t.avatar + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x42" + n = n + 1; out[n] = wire.encode_bytes(v) + end + -- field 9: user_id + v = t.user_id + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x49" + n = n + 1; out[n] = wire.encode_fixed64(v) + end + -- field 10: balance + v = t.balance + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x50" + n = n + 1; out[n] = wire.encode_sint32(v) + end + -- field 11: weight_kg + v = t.weight_kg + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x59" + n = n + 1; out[n] = wire.encode_double(v) + end + -- field 13: ages_by_nickname + v = t.ages_by_nickname + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x6a", "\x0a", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_int32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 14: nickname_by_age + v = t.nickname_by_age + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x72", "\x08", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_int32(_k) + end + if _val ~= '' then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_string(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 15: addresses_by_label + v = t.addresses_by_label + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x7a", "\x0a", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= nil then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_len(M.Address_encode(_val)) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.Person_decode(buf) + if type(buf) ~= 'string' then + error("expected string for hello.Person decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_string(buf, pos) + result.name = val + elseif id == 2 then + local val + val, pos = wire.decode_int32(buf, pos) + result.age = val + elseif id == 3 then + local list = result.emails + if list == nil then list = {}; result.emails = list end + local val + val, pos = wire.decode_string(buf, pos) + list[#list + 1] = val + elseif id == 4 then + local u + u, pos = wire.decode_varint(buf, pos) + result.status = tonumber(u) + elseif id == 5 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.address + if prev == nil then + result.address = M.Address_decode(payload) + else + local new = M.Address_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 6 then + local list = result.friends + if list == nil then list = {}; result.friends = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = M.Person_decode(payload) + elseif id == 7 then + local list = result.lucky_numbers + if list == nil then list = {}; result.lucky_numbers = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int32(buf, pos) + list[#list + 1] = val + end + elseif id == 8 then + local val + val, pos = wire.decode_bytes(buf, pos) + result.avatar = val + elseif id == 9 then + local val + val, pos = wire.decode_fixed64(buf, pos) + result.user_id = val + elseif id == 10 then + local val + val, pos = wire.decode_sint32(buf, pos) + result.balance = val + elseif id == 11 then + local val + val, pos = wire.decode_double(buf, pos) + result.weight_kg = val + elseif id == 13 then + local map = result.ages_by_nickname + if map == nil then map = {}; result.ages_by_nickname = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_int32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 14 then + local map = result.nickname_by_age + if map == nil then map = {}; result.nickname_by_age = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, '' + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_int32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_string(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 15 then + local map = result.addresses_by_label + if map == nil then map = {}; result.addresses_by_label = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', {} + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + local _payload + _payload, _ep = wire.decode_len(payload, _ep) + _val = M.Address_decode(_payload) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +-- Service: hello.Greeter +M.Greeter_service = { + name = "hello.Greeter", + full_name = "/hello.Greeter", + methods = { + SayHello = { + name = "SayHello", + full_name = "/hello.Greeter/SayHello", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + }, + Echo = { + name = "Echo", + full_name = "/hello.Greeter/Echo", + input = M.HelloRequest_descriptor, + output = M.HelloRequest_descriptor, + }, + StreamHellos = { + name = "StreamHellos", + full_name = "/hello.Greeter/StreamHellos", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + server_streaming = true, + }, + CollectHellos = { + name = "CollectHellos", + full_name = "/hello.Greeter/CollectHellos", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + client_streaming = true, + }, + Chat = { + name = "Chat", + full_name = "/hello.Greeter/Chat", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + client_streaming = true, + server_streaming = true, + }, + }, +} + +function M.Greeter_client(transport) + if transport == nil then error("Greeter_client: transport is required", 0) end + return { + SayHello = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local resp_bytes = transport:unary("/hello.Greeter/SayHello", req_bytes, ctx) + return M.HelloReply_decode(resp_bytes) + end, + Echo = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local resp_bytes = transport:unary("/hello.Greeter/Echo", req_bytes, ctx) + return M.HelloRequest_decode(resp_bytes) + end, + StreamHellos = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local raw = transport:server_stream("/hello.Greeter/StreamHellos", req_bytes, ctx) + return pb.grpc.wrap_server_stream(raw, M.HelloReply_decode) + end, + CollectHellos = function(ctx) + local raw = transport:client_stream("/hello.Greeter/CollectHellos", ctx) + return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) + end, + Chat = function(ctx) + local raw = transport:bidi("/hello.Greeter/Chat", ctx) + return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) + end, + } +end + +function M.Greeter_server(impl) + if type(impl) ~= 'table' then error("Greeter_server: impl table is required", 0) end + return { + service = M.Greeter_service, + methods = { + ["/hello.Greeter/SayHello"] = function(req_bytes, ctx) + local handler = impl.SayHello + if handler == nil then error("Greeter.SayHello: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local resp = handler(req, ctx) + return M.HelloReply_encode(resp) + end, + ["/hello.Greeter/Echo"] = function(req_bytes, ctx) + local handler = impl.Echo + if handler == nil then error("Greeter.Echo: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local resp = handler(req, ctx) + return M.HelloRequest_encode(resp) + end, + }, + streams = { + ["/hello.Greeter/StreamHellos"] = { + kind = 'server_stream', + handler = function(req_bytes, server_view, ctx) + local handler = impl.StreamHellos + if handler == nil then error("Greeter.StreamHellos: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local wrapped = pb.grpc.wrap_server_view(server_view, nil, M.HelloReply_encode) + handler(req, wrapped, ctx) + end, + }, + ["/hello.Greeter/CollectHellos"] = { + kind = 'client_stream', + handler = function(_, server_view, ctx) + local handler = impl.CollectHellos + if handler == nil then error("Greeter.CollectHellos: handler missing", 0) end + local wrapped = pb.grpc.wrap_server_view(server_view, M.HelloRequest_decode, nil) + local resp = handler(wrapped, ctx) + if resp == nil then error("Greeter.CollectHellos: handler returned nil response", 0) end + server_view:send(M.HelloReply_encode(resp)) + end, + }, + ["/hello.Greeter/Chat"] = { + kind = 'bidi', + handler = function(_, server_view, ctx) + local handler = impl.Chat + if handler == nil then error("Greeter.Chat: handler missing", 0) end + local wrapped = pb.grpc.wrap_server_view(server_view, M.HelloRequest_decode, M.HelloReply_encode) + handler(wrapped, ctx) + end, + }, + }, + } +end + +return M diff --git a/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..ed51e53793e31b1851bca3fc98bca803563a8d29 --- /dev/null +++ b/examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -0,0 +1,3621 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: test_messages_proto3.proto +-- syntax: proto3 +-- package: protobuf_test_messages.proto3 + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: protobuf_test_messages.proto3.ForeignEnum +M.ForeignEnum_descriptor = pb.enum("protobuf_test_messages.proto3.ForeignEnum", { + FOREIGN_FOO = 0, + FOREIGN_BAR = 1, + FOREIGN_BAZ = 2, +}) +M.ForeignEnum = M.ForeignEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum +M.TestAllTypesProto3_NestedEnum_descriptor = pb.enum("protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", { + FOO = 0, + BAR = 1, + BAZ = 2, + NEG = -1, +}) +M.TestAllTypesProto3_NestedEnum = M.TestAllTypesProto3_NestedEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum +M.TestAllTypesProto3_AliasedEnum_descriptor = pb.enum("protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum", { + ALIAS_FOO = 0, + ALIAS_BAR = 1, + ALIAS_BAZ = 2, + MOO = 2, + moo = 2, + bAz = 2, +}) +M.TestAllTypesProto3_AliasedEnum = M.TestAllTypesProto3_AliasedEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.EnumOnlyProto3.Bool +M.EnumOnlyProto3_Bool_descriptor = pb.enum("protobuf_test_messages.proto3.EnumOnlyProto3.Bool", { + kFalse = 0, + kTrue = 1, +}) +M.EnumOnlyProto3_Bool = M.EnumOnlyProto3_Bool_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.TestAllTypesProto3_descriptor = {name = "protobuf_test_messages.proto3.TestAllTypesProto3"} +M.TestAllTypesProto3_NestedMessage_descriptor = {name = "protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage"} +M.ForeignMessage_descriptor = {name = "protobuf_test_messages.proto3.ForeignMessage"} +M.NullHypothesisProto3_descriptor = {name = "protobuf_test_messages.proto3.NullHypothesisProto3"} +M.EnumOnlyProto3_descriptor = {name = "protobuf_test_messages.proto3.EnumOnlyProto3"} + +-- Message: protobuf_test_messages.proto3.TestAllTypesProto3 +M.TestAllTypesProto3_descriptor.fields = { + {name="optional_int32", id=1, kind='scalar', proto_type="int32"}, + {name="optional_int64", id=2, kind='scalar', proto_type="int64"}, + {name="optional_uint32", id=3, kind='scalar', proto_type="uint32"}, + {name="optional_uint64", id=4, kind='scalar', proto_type="uint64"}, + {name="optional_sint32", id=5, kind='scalar', proto_type="sint32"}, + {name="optional_sint64", id=6, kind='scalar', proto_type="sint64"}, + {name="optional_fixed32", id=7, kind='scalar', proto_type="fixed32"}, + {name="optional_fixed64", id=8, kind='scalar', proto_type="fixed64"}, + {name="optional_sfixed32", id=9, kind='scalar', proto_type="sfixed32"}, + {name="optional_sfixed64", id=10, kind='scalar', proto_type="sfixed64"}, + {name="optional_float", id=11, kind='scalar', proto_type="float"}, + {name="optional_double", id=12, kind='scalar', proto_type="double"}, + {name="optional_bool", id=13, kind='scalar', proto_type="bool"}, + {name="optional_string", id=14, kind='scalar', proto_type="string"}, + {name="optional_bytes", id=15, kind='scalar', proto_type="bytes"}, + {name="optional_nested_message", id=18, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor}, + {name="optional_foreign_message", id=19, kind='message', message=M.ForeignMessage_descriptor}, + {name="optional_nested_enum", id=21, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor}, + {name="optional_foreign_enum", id=22, kind='enum', enum=M.ForeignEnum_descriptor}, + {name="optional_aliased_enum", id=23, kind='enum', enum=M.TestAllTypesProto3_AliasedEnum_descriptor}, + {name="optional_string_piece", id=24, kind='scalar', proto_type="string"}, + {name="optional_cord", id=25, kind='scalar', proto_type="string"}, + {name="recursive_message", id=27, kind='message', message=M.TestAllTypesProto3_descriptor}, + {name="repeated_int32", id=31, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="repeated_int64", id=32, kind='scalar', proto_type="int64", repeated=true, packed=true}, + {name="repeated_uint32", id=33, kind='scalar', proto_type="uint32", repeated=true, packed=true}, + {name="repeated_uint64", id=34, kind='scalar', proto_type="uint64", repeated=true, packed=true}, + {name="repeated_sint32", id=35, kind='scalar', proto_type="sint32", repeated=true, packed=true}, + {name="repeated_sint64", id=36, kind='scalar', proto_type="sint64", repeated=true, packed=true}, + {name="repeated_fixed32", id=37, kind='scalar', proto_type="fixed32", repeated=true, packed=true}, + {name="repeated_fixed64", id=38, kind='scalar', proto_type="fixed64", repeated=true, packed=true}, + {name="repeated_sfixed32", id=39, kind='scalar', proto_type="sfixed32", repeated=true, packed=true}, + {name="repeated_sfixed64", id=40, kind='scalar', proto_type="sfixed64", repeated=true, packed=true}, + {name="repeated_float", id=41, kind='scalar', proto_type="float", repeated=true, packed=true}, + {name="repeated_double", id=42, kind='scalar', proto_type="double", repeated=true, packed=true}, + {name="repeated_bool", id=43, kind='scalar', proto_type="bool", repeated=true, packed=true}, + {name="repeated_string", id=44, kind='scalar', proto_type="string", repeated=true}, + {name="repeated_bytes", id=45, kind='scalar', proto_type="bytes", repeated=true}, + {name="repeated_nested_message", id=48, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor, repeated=true}, + {name="repeated_foreign_message", id=49, kind='message', message=M.ForeignMessage_descriptor, repeated=true}, + {name="repeated_nested_enum", id=51, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true}, + {name="repeated_foreign_enum", id=52, kind='enum', enum=M.ForeignEnum_descriptor, repeated=true, packed=true}, + {name="repeated_string_piece", id=54, kind='scalar', proto_type="string", repeated=true}, + {name="repeated_cord", id=55, kind='scalar', proto_type="string", repeated=true}, + {name="packed_int32", id=75, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="packed_int64", id=76, kind='scalar', proto_type="int64", repeated=true, packed=true}, + {name="packed_uint32", id=77, kind='scalar', proto_type="uint32", repeated=true, packed=true}, + {name="packed_uint64", id=78, kind='scalar', proto_type="uint64", repeated=true, packed=true}, + {name="packed_sint32", id=79, kind='scalar', proto_type="sint32", repeated=true, packed=true}, + {name="packed_sint64", id=80, kind='scalar', proto_type="sint64", repeated=true, packed=true}, + {name="packed_fixed32", id=81, kind='scalar', proto_type="fixed32", repeated=true, packed=true}, + {name="packed_fixed64", id=82, kind='scalar', proto_type="fixed64", repeated=true, packed=true}, + {name="packed_sfixed32", id=83, kind='scalar', proto_type="sfixed32", repeated=true, packed=true}, + {name="packed_sfixed64", id=84, kind='scalar', proto_type="sfixed64", repeated=true, packed=true}, + {name="packed_float", id=85, kind='scalar', proto_type="float", repeated=true, packed=true}, + {name="packed_double", id=86, kind='scalar', proto_type="double", repeated=true, packed=true}, + {name="packed_bool", id=87, kind='scalar', proto_type="bool", repeated=true, packed=true}, + {name="packed_nested_enum", id=88, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true}, + {name="unpacked_int32", id=89, kind='scalar', proto_type="int32", repeated=true, packed=false}, + {name="unpacked_int64", id=90, kind='scalar', proto_type="int64", repeated=true, packed=false}, + {name="unpacked_uint32", id=91, kind='scalar', proto_type="uint32", repeated=true, packed=false}, + {name="unpacked_uint64", id=92, kind='scalar', proto_type="uint64", repeated=true, packed=false}, + {name="unpacked_sint32", id=93, kind='scalar', proto_type="sint32", repeated=true, packed=false}, + {name="unpacked_sint64", id=94, kind='scalar', proto_type="sint64", repeated=true, packed=false}, + {name="unpacked_fixed32", id=95, kind='scalar', proto_type="fixed32", repeated=true, packed=false}, + {name="unpacked_fixed64", id=96, kind='scalar', proto_type="fixed64", repeated=true, packed=false}, + {name="unpacked_sfixed32", id=97, kind='scalar', proto_type="sfixed32", repeated=true, packed=false}, + {name="unpacked_sfixed64", id=98, kind='scalar', proto_type="sfixed64", repeated=true, packed=false}, + {name="unpacked_float", id=99, kind='scalar', proto_type="float", repeated=true, packed=false}, + {name="unpacked_double", id=100, kind='scalar', proto_type="double", repeated=true, packed=false}, + {name="unpacked_bool", id=101, kind='scalar', proto_type="bool", repeated=true, packed=false}, + {name="unpacked_nested_enum", id=102, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=false}, + {name="map_int32_int32", id=56, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="int32"}}, + {name="map_int64_int64", id=57, kind='map', key={kind='scalar', proto_type="int64"}, value={kind='scalar', proto_type="int64"}}, + {name="map_uint32_uint32", id=58, kind='map', key={kind='scalar', proto_type="uint32"}, value={kind='scalar', proto_type="uint32"}}, + {name="map_uint64_uint64", id=59, kind='map', key={kind='scalar', proto_type="uint64"}, value={kind='scalar', proto_type="uint64"}}, + {name="map_sint32_sint32", id=60, kind='map', key={kind='scalar', proto_type="sint32"}, value={kind='scalar', proto_type="sint32"}}, + {name="map_sint64_sint64", id=61, kind='map', key={kind='scalar', proto_type="sint64"}, value={kind='scalar', proto_type="sint64"}}, + {name="map_fixed32_fixed32", id=62, kind='map', key={kind='scalar', proto_type="fixed32"}, value={kind='scalar', proto_type="fixed32"}}, + {name="map_fixed64_fixed64", id=63, kind='map', key={kind='scalar', proto_type="fixed64"}, value={kind='scalar', proto_type="fixed64"}}, + {name="map_sfixed32_sfixed32", id=64, kind='map', key={kind='scalar', proto_type="sfixed32"}, value={kind='scalar', proto_type="sfixed32"}}, + {name="map_sfixed64_sfixed64", id=65, kind='map', key={kind='scalar', proto_type="sfixed64"}, value={kind='scalar', proto_type="sfixed64"}}, + {name="map_int32_float", id=66, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="float"}}, + {name="map_int32_double", id=67, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="double"}}, + {name="map_bool_bool", id=68, kind='map', key={kind='scalar', proto_type="bool"}, value={kind='scalar', proto_type="bool"}}, + {name="map_string_string", id=69, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="string"}}, + {name="map_string_bytes", id=70, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="bytes"}}, + {name="map_string_nested_message", id=71, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor}}, + {name="map_string_foreign_message", id=72, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.ForeignMessage_descriptor}}, + {name="map_string_nested_enum", id=73, kind='map', key={kind='scalar', proto_type="string"}, value={kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor}}, + {name="map_string_foreign_enum", id=74, kind='map', key={kind='scalar', proto_type="string"}, value={kind='enum', enum=M.ForeignEnum_descriptor}}, + {name="oneof_uint32", id=111, kind='scalar', proto_type="uint32", oneof="oneof_field"}, + {name="oneof_nested_message", id=112, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor, oneof="oneof_field"}, + {name="oneof_string", id=113, kind='scalar', proto_type="string", oneof="oneof_field"}, + {name="oneof_bytes", id=114, kind='scalar', proto_type="bytes", oneof="oneof_field"}, + {name="oneof_bool", id=115, kind='scalar', proto_type="bool", oneof="oneof_field"}, + {name="oneof_uint64", id=116, kind='scalar', proto_type="uint64", oneof="oneof_field"}, + {name="oneof_float", id=117, kind='scalar', proto_type="float", oneof="oneof_field"}, + {name="oneof_double", id=118, kind='scalar', proto_type="double", oneof="oneof_field"}, + {name="oneof_enum", id=119, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, oneof="oneof_field"}, + {name="oneof_null_value", id=120, kind='enum', enum=pb.wkt.NullValue_descriptor, oneof="oneof_field"}, + {name="optional_bool_wrapper", id=201, kind='message', message=pb.wkt.BoolValue_descriptor}, + {name="optional_int32_wrapper", id=202, kind='message', message=pb.wkt.Int32Value_descriptor}, + {name="optional_int64_wrapper", id=203, kind='message', message=pb.wkt.Int64Value_descriptor}, + {name="optional_uint32_wrapper", id=204, kind='message', message=pb.wkt.UInt32Value_descriptor}, + {name="optional_uint64_wrapper", id=205, kind='message', message=pb.wkt.UInt64Value_descriptor}, + {name="optional_float_wrapper", id=206, kind='message', message=pb.wkt.FloatValue_descriptor}, + {name="optional_double_wrapper", id=207, kind='message', message=pb.wkt.DoubleValue_descriptor}, + {name="optional_string_wrapper", id=208, kind='message', message=pb.wkt.StringValue_descriptor}, + {name="optional_bytes_wrapper", id=209, kind='message', message=pb.wkt.BytesValue_descriptor}, + {name="repeated_bool_wrapper", id=211, kind='message', message=pb.wkt.BoolValue_descriptor, repeated=true}, + {name="repeated_int32_wrapper", id=212, kind='message', message=pb.wkt.Int32Value_descriptor, repeated=true}, + {name="repeated_int64_wrapper", id=213, kind='message', message=pb.wkt.Int64Value_descriptor, repeated=true}, + {name="repeated_uint32_wrapper", id=214, kind='message', message=pb.wkt.UInt32Value_descriptor, repeated=true}, + {name="repeated_uint64_wrapper", id=215, kind='message', message=pb.wkt.UInt64Value_descriptor, repeated=true}, + {name="repeated_float_wrapper", id=216, kind='message', message=pb.wkt.FloatValue_descriptor, repeated=true}, + {name="repeated_double_wrapper", id=217, kind='message', message=pb.wkt.DoubleValue_descriptor, repeated=true}, + {name="repeated_string_wrapper", id=218, kind='message', message=pb.wkt.StringValue_descriptor, repeated=true}, + {name="repeated_bytes_wrapper", id=219, kind='message', message=pb.wkt.BytesValue_descriptor, repeated=true}, + {name="optional_duration", id=301, kind='message', message=pb.wkt.Duration_descriptor}, + {name="optional_timestamp", id=302, kind='message', message=pb.wkt.Timestamp_descriptor}, + {name="optional_field_mask", id=303, kind='message', message=pb.wkt.FieldMask_descriptor}, + {name="optional_struct", id=304, kind='message', message=pb.wkt.Struct_descriptor}, + {name="optional_any", id=305, kind='message', message=pb.wkt.Any_descriptor}, + {name="optional_value", id=306, kind='message', message=pb.wkt.Value_descriptor}, + {name="optional_null_value", id=307, kind='enum', enum=pb.wkt.NullValue_descriptor}, + {name="optional_empty", id=308, kind='message', message=pb.wkt.Empty_descriptor}, + {name="repeated_duration", id=311, kind='message', message=pb.wkt.Duration_descriptor, repeated=true}, + {name="repeated_timestamp", id=312, kind='message', message=pb.wkt.Timestamp_descriptor, repeated=true}, + {name="repeated_fieldmask", id=313, kind='message', message=pb.wkt.FieldMask_descriptor, repeated=true}, + {name="repeated_struct", id=324, kind='message', message=pb.wkt.Struct_descriptor, repeated=true}, + {name="repeated_any", id=315, kind='message', message=pb.wkt.Any_descriptor, repeated=true}, + {name="repeated_value", id=316, kind='message', message=pb.wkt.Value_descriptor, repeated=true}, + {name="repeated_list_value", id=317, kind='message', message=pb.wkt.ListValue_descriptor, repeated=true}, + {name="repeated_empty", id=318, kind='message', message=pb.wkt.Empty_descriptor, repeated=true}, + {name="fieldname1", id=401, kind='scalar', proto_type="int32"}, + {name="field_name2", id=402, kind='scalar', proto_type="int32"}, + {name="_field_name3", id=403, kind='scalar', proto_type="int32"}, + {name="field__name4_", id=404, kind='scalar', proto_type="int32"}, + {name="field0name5", id=405, kind='scalar', proto_type="int32"}, + {name="field_0_name6", id=406, kind='scalar', proto_type="int32"}, + {name="fieldName7", id=407, kind='scalar', proto_type="int32"}, + {name="FieldName8", id=408, kind='scalar', proto_type="int32"}, + {name="field_Name9", id=409, kind='scalar', proto_type="int32"}, + {name="Field_Name10", id=410, kind='scalar', proto_type="int32"}, + {name="FIELD_NAME11", id=411, kind='scalar', proto_type="int32"}, + {name="FIELD_name12", id=412, kind='scalar', proto_type="int32"}, + {name="__field_name13", id=413, kind='scalar', proto_type="int32"}, + {name="__Field_name14", id=414, kind='scalar', proto_type="int32"}, + {name="field__name15", id=415, kind='scalar', proto_type="int32"}, + {name="field__Name16", id=416, kind='scalar', proto_type="int32"}, + {name="field_name17__", id=417, kind='scalar', proto_type="int32"}, + {name="Field_name18__", id=418, kind='scalar', proto_type="int32"}, +} +M.TestAllTypesProto3_descriptor.oneofs = { + oneof_field = {"oneof_uint32", "oneof_nested_message", "oneof_string", "oneof_bytes", "oneof_bool", "oneof_uint64", "oneof_float", "oneof_double", "oneof_enum", "oneof_null_value"}, +} +pb.finalize_message(M.TestAllTypesProto3_descriptor) + +-- Message: protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage +M.TestAllTypesProto3_NestedMessage_descriptor.fields = { + {name="a", id=1, kind='scalar', proto_type="int32"}, + {name="corecursive", id=2, kind='message', message=M.TestAllTypesProto3_descriptor}, +} +pb.finalize_message(M.TestAllTypesProto3_NestedMessage_descriptor) + +-- Message: protobuf_test_messages.proto3.ForeignMessage +M.ForeignMessage_descriptor.fields = { + {name="c", id=1, kind='scalar', proto_type="int32"}, +} +pb.finalize_message(M.ForeignMessage_descriptor) + +-- Message: protobuf_test_messages.proto3.NullHypothesisProto3 +M.NullHypothesisProto3_descriptor.fields = { +} +pb.finalize_message(M.NullHypothesisProto3_descriptor) + +-- Message: protobuf_test_messages.proto3.EnumOnlyProto3 +M.EnumOnlyProto3_descriptor.fields = { +} +pb.finalize_message(M.EnumOnlyProto3_descriptor) + +function M.TestAllTypesProto3_new(t) return t or {} end + +function M.TestAllTypesProto3_encode(t) + if type(t) ~= 'table' then + error("expected table for protobuf_test_messages.proto3.TestAllTypesProto3, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _of_oneof_field + if t.oneof_uint32 ~= nil then _of_oneof_field = "oneof_uint32" end + if t.oneof_nested_message ~= nil then _of_oneof_field = "oneof_nested_message" end + if t.oneof_string ~= nil then _of_oneof_field = "oneof_string" end + if t.oneof_bytes ~= nil then _of_oneof_field = "oneof_bytes" end + if t.oneof_bool ~= nil then _of_oneof_field = "oneof_bool" end + if t.oneof_uint64 ~= nil then _of_oneof_field = "oneof_uint64" end + if t.oneof_float ~= nil then _of_oneof_field = "oneof_float" end + if t.oneof_double ~= nil then _of_oneof_field = "oneof_double" end + if t.oneof_enum ~= nil then _of_oneof_field = "oneof_enum" end + if t.oneof_null_value ~= nil then _of_oneof_field = "oneof_null_value" end + -- field 1: optional_int32 + v = t.optional_int32 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 2: optional_int64 + v = t.optional_int64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x10" + n = n + 1; out[n] = wire.encode_int64(v) + end + -- field 3: optional_uint32 + v = t.optional_uint32 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x18" + n = n + 1; out[n] = wire.encode_uint32(v) + end + -- field 4: optional_uint64 + v = t.optional_uint64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x20" + n = n + 1; out[n] = wire.encode_uint64(v) + end + -- field 5: optional_sint32 + v = t.optional_sint32 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x28" + n = n + 1; out[n] = wire.encode_sint32(v) + end + -- field 6: optional_sint64 + v = t.optional_sint64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x30" + n = n + 1; out[n] = wire.encode_sint64(v) + end + -- field 7: optional_fixed32 + v = t.optional_fixed32 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x3d" + n = n + 1; out[n] = wire.encode_fixed32(v) + end + -- field 8: optional_fixed64 + v = t.optional_fixed64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x41" + n = n + 1; out[n] = wire.encode_fixed64(v) + end + -- field 9: optional_sfixed32 + v = t.optional_sfixed32 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x4d" + n = n + 1; out[n] = wire.encode_sfixed32(v) + end + -- field 10: optional_sfixed64 + v = t.optional_sfixed64 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x51" + n = n + 1; out[n] = wire.encode_sfixed64(v) + end + -- field 11: optional_float + v = t.optional_float + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x5d" + n = n + 1; out[n] = wire.encode_float(v) + end + -- field 12: optional_double + v = t.optional_double + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x61" + n = n + 1; out[n] = wire.encode_double(v) + end + -- field 13: optional_bool + v = t.optional_bool + if v ~= nil and v ~= false then + n = n + 1; out[n] = "\x68" + n = n + 1; out[n] = wire.encode_bool(v) + end + -- field 14: optional_string + v = t.optional_string + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x72" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 15: optional_bytes + v = t.optional_bytes + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\x7a" + n = n + 1; out[n] = wire.encode_bytes(v) + end + -- field 18: optional_nested_message + v = t.optional_nested_message + if v ~= nil then + n = n + 1; out[n] = "\x92\x01" + n = n + 1; out[n] = wire.encode_len(M.TestAllTypesProto3_NestedMessage_encode(v)) + end + -- field 19: optional_foreign_message + v = t.optional_foreign_message + if v ~= nil then + n = n + 1; out[n] = "\x9a\x01" + n = n + 1; out[n] = wire.encode_len(M.ForeignMessage_encode(v)) + end + -- field 21: optional_nested_enum + v = t.optional_nested_enum + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.TestAllTypesProto3_NestedEnum[v] + if nv == nil then error("unknown enum value '" .. v .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\xa8\x01" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 22: optional_foreign_enum + v = t.optional_foreign_enum + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.ForeignEnum[v] + if nv == nil then error("unknown enum value '" .. v .. "' for protobuf_test_messages.proto3.ForeignEnum", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\xb0\x01" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 23: optional_aliased_enum + v = t.optional_aliased_enum + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = M.TestAllTypesProto3_AliasedEnum[v] + if nv == nil then error("unknown enum value '" .. v .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\xb8\x01" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 24: optional_string_piece + v = t.optional_string_piece + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\xc2\x01" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 25: optional_cord + v = t.optional_cord + if v ~= nil and v ~= '' then + n = n + 1; out[n] = "\xca\x01" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 27: recursive_message + v = t.recursive_message + if v ~= nil then + n = n + 1; out[n] = "\xda\x01" + n = n + 1; out[n] = wire.encode_len(M.TestAllTypesProto3_encode(v)) + end + -- field 31: repeated_int32 + v = t.repeated_int32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_int32(v[_i]) + end + n = n + 1; out[n] = "\xfa\x01" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 32: repeated_int64 + v = t.repeated_int64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_int64(v[_i]) + end + n = n + 1; out[n] = "\x82\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 33: repeated_uint32 + v = t.repeated_uint32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_uint32(v[_i]) + end + n = n + 1; out[n] = "\x8a\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 34: repeated_uint64 + v = t.repeated_uint64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_uint64(v[_i]) + end + n = n + 1; out[n] = "\x92\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 35: repeated_sint32 + v = t.repeated_sint32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sint32(v[_i]) + end + n = n + 1; out[n] = "\x9a\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 36: repeated_sint64 + v = t.repeated_sint64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sint64(v[_i]) + end + n = n + 1; out[n] = "\xa2\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 37: repeated_fixed32 + v = t.repeated_fixed32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_fixed32(v[_i]) + end + n = n + 1; out[n] = "\xaa\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 38: repeated_fixed64 + v = t.repeated_fixed64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_fixed64(v[_i]) + end + n = n + 1; out[n] = "\xb2\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 39: repeated_sfixed32 + v = t.repeated_sfixed32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sfixed32(v[_i]) + end + n = n + 1; out[n] = "\xba\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 40: repeated_sfixed64 + v = t.repeated_sfixed64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sfixed64(v[_i]) + end + n = n + 1; out[n] = "\xc2\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 41: repeated_float + v = t.repeated_float + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_float(v[_i]) + end + n = n + 1; out[n] = "\xca\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 42: repeated_double + v = t.repeated_double + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_double(v[_i]) + end + n = n + 1; out[n] = "\xd2\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 43: repeated_bool + v = t.repeated_bool + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_bool(v[_i]) + end + n = n + 1; out[n] = "\xda\x02" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 44: repeated_string + v = t.repeated_string + if v ~= nil and #v > 0 then + local _tag = "\xe2\x02" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_string(v[_i]) + end + end + -- field 45: repeated_bytes + v = t.repeated_bytes + if v ~= nil and #v > 0 then + local _tag = "\xea\x02" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_bytes(v[_i]) + end + end + -- field 48: repeated_nested_message + v = t.repeated_nested_message + if v ~= nil and #v > 0 then + local _tag = "\x82\x03" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(M.TestAllTypesProto3_NestedMessage_encode(v[_i])) + end + end + -- field 49: repeated_foreign_message + v = t.repeated_foreign_message + if v ~= nil and #v > 0 then + local _tag = "\x8a\x03" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(M.ForeignMessage_encode(v[_i])) + end + end + -- field 51: repeated_nested_enum + v = t.repeated_nested_enum + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + local elem = v[_i] + local nv = elem + if type(elem) == 'string' then + nv = M.TestAllTypesProto3_NestedEnum[elem] + if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + m = m + 1; parts[m] = wire.encode_int32(nv) + end + n = n + 1; out[n] = "\x9a\x03" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 52: repeated_foreign_enum + v = t.repeated_foreign_enum + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + local elem = v[_i] + local nv = elem + if type(elem) == 'string' then + nv = M.ForeignEnum[elem] + if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.ForeignEnum", 0) end + end + m = m + 1; parts[m] = wire.encode_int32(nv) + end + n = n + 1; out[n] = "\xa2\x03" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 54: repeated_string_piece + v = t.repeated_string_piece + if v ~= nil and #v > 0 then + local _tag = "\xb2\x03" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_string(v[_i]) + end + end + -- field 55: repeated_cord + v = t.repeated_cord + if v ~= nil and #v > 0 then + local _tag = "\xba\x03" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_string(v[_i]) + end + end + -- field 75: packed_int32 + v = t.packed_int32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_int32(v[_i]) + end + n = n + 1; out[n] = "\xda\x04" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 76: packed_int64 + v = t.packed_int64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_int64(v[_i]) + end + n = n + 1; out[n] = "\xe2\x04" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 77: packed_uint32 + v = t.packed_uint32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_uint32(v[_i]) + end + n = n + 1; out[n] = "\xea\x04" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 78: packed_uint64 + v = t.packed_uint64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_uint64(v[_i]) + end + n = n + 1; out[n] = "\xf2\x04" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 79: packed_sint32 + v = t.packed_sint32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sint32(v[_i]) + end + n = n + 1; out[n] = "\xfa\x04" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 80: packed_sint64 + v = t.packed_sint64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sint64(v[_i]) + end + n = n + 1; out[n] = "\x82\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 81: packed_fixed32 + v = t.packed_fixed32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_fixed32(v[_i]) + end + n = n + 1; out[n] = "\x8a\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 82: packed_fixed64 + v = t.packed_fixed64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_fixed64(v[_i]) + end + n = n + 1; out[n] = "\x92\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 83: packed_sfixed32 + v = t.packed_sfixed32 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sfixed32(v[_i]) + end + n = n + 1; out[n] = "\x9a\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 84: packed_sfixed64 + v = t.packed_sfixed64 + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_sfixed64(v[_i]) + end + n = n + 1; out[n] = "\xa2\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 85: packed_float + v = t.packed_float + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_float(v[_i]) + end + n = n + 1; out[n] = "\xaa\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 86: packed_double + v = t.packed_double + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_double(v[_i]) + end + n = n + 1; out[n] = "\xb2\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 87: packed_bool + v = t.packed_bool + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + m = m + 1; parts[m] = wire.encode_bool(v[_i]) + end + n = n + 1; out[n] = "\xba\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 88: packed_nested_enum + v = t.packed_nested_enum + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + local elem = v[_i] + local nv = elem + if type(elem) == 'string' then + nv = M.TestAllTypesProto3_NestedEnum[elem] + if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + m = m + 1; parts[m] = wire.encode_int32(nv) + end + n = n + 1; out[n] = "\xc2\x05" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 89: unpacked_int32 + v = t.unpacked_int32 + if v ~= nil and #v > 0 then + local _tag = "\xc8\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_int32(v[_i]) + end + end + -- field 90: unpacked_int64 + v = t.unpacked_int64 + if v ~= nil and #v > 0 then + local _tag = "\xd0\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_int64(v[_i]) + end + end + -- field 91: unpacked_uint32 + v = t.unpacked_uint32 + if v ~= nil and #v > 0 then + local _tag = "\xd8\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_uint32(v[_i]) + end + end + -- field 92: unpacked_uint64 + v = t.unpacked_uint64 + if v ~= nil and #v > 0 then + local _tag = "\xe0\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_uint64(v[_i]) + end + end + -- field 93: unpacked_sint32 + v = t.unpacked_sint32 + if v ~= nil and #v > 0 then + local _tag = "\xe8\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_sint32(v[_i]) + end + end + -- field 94: unpacked_sint64 + v = t.unpacked_sint64 + if v ~= nil and #v > 0 then + local _tag = "\xf0\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_sint64(v[_i]) + end + end + -- field 95: unpacked_fixed32 + v = t.unpacked_fixed32 + if v ~= nil and #v > 0 then + local _tag = "\xfd\x05" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_fixed32(v[_i]) + end + end + -- field 96: unpacked_fixed64 + v = t.unpacked_fixed64 + if v ~= nil and #v > 0 then + local _tag = "\x81\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_fixed64(v[_i]) + end + end + -- field 97: unpacked_sfixed32 + v = t.unpacked_sfixed32 + if v ~= nil and #v > 0 then + local _tag = "\x8d\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_sfixed32(v[_i]) + end + end + -- field 98: unpacked_sfixed64 + v = t.unpacked_sfixed64 + if v ~= nil and #v > 0 then + local _tag = "\x91\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_sfixed64(v[_i]) + end + end + -- field 99: unpacked_float + v = t.unpacked_float + if v ~= nil and #v > 0 then + local _tag = "\x9d\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_float(v[_i]) + end + end + -- field 100: unpacked_double + v = t.unpacked_double + if v ~= nil and #v > 0 then + local _tag = "\xa1\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_double(v[_i]) + end + end + -- field 101: unpacked_bool + v = t.unpacked_bool + if v ~= nil and #v > 0 then + local _tag = "\xa8\x06" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_bool(v[_i]) + end + end + -- field 102: unpacked_nested_enum + v = t.unpacked_nested_enum + if v ~= nil and #v > 0 then + local parts, m = {}, 0 + for _i = 1, #v do + local elem = v[_i] + local nv = elem + if type(elem) == 'string' then + nv = M.TestAllTypesProto3_NestedEnum[elem] + if nv == nil then error("unknown enum value '" .. elem .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + m = m + 1; parts[m] = wire.encode_int32(nv) + end + n = n + 1; out[n] = "\xb0\x06" + n = n + 1; out[n] = wire.encode_len(table.concat(parts)) + end + -- field 56: map_int32_int32 + v = t.map_int32_int32 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xc2\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_int32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_int32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 57: map_int64_int64 + v = t.map_int64_int64 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xca\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_int64(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_int64(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 58: map_uint32_uint32 + v = t.map_uint32_uint32 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xd2\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_uint32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_uint32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 59: map_uint64_uint64 + v = t.map_uint64_uint64 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xda\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_uint64(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_uint64(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 60: map_sint32_sint32 + v = t.map_sint32_sint32 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xe2\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_sint32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_sint32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 61: map_sint64_sint64 + v = t.map_sint64_sint64 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xea\x03", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_sint64(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_sint64(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 62: map_fixed32_fixed32 + v = t.map_fixed32_fixed32 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xf2\x03", "\x0d", "\x15" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_fixed32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_fixed32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 63: map_fixed64_fixed64 + v = t.map_fixed64_fixed64 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xfa\x03", "\x09", "\x11" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_fixed64(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_fixed64(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 64: map_sfixed32_sfixed32 + v = t.map_sfixed32_sfixed32 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x82\x04", "\x0d", "\x15" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_sfixed32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_sfixed32(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 65: map_sfixed64_sfixed64 + v = t.map_sfixed64_sfixed64 + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x8a\x04", "\x09", "\x11" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_sfixed64(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_sfixed64(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 66: map_int32_float + v = t.map_int32_float + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x92\x04", "\x08", "\x15" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_int32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_float(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 67: map_int32_double + v = t.map_int32_double + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\x9a\x04", "\x08", "\x11" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= 0 then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_int32(_k) + end + if _val ~= 0 then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_double(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 68: map_bool_bool + v = t.map_bool_bool + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xa2\x04", "\x08", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= false then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_bool(_k) + end + if _val ~= false then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_bool(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 69: map_string_string + v = t.map_string_string + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xaa\x04", "\x0a", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= '' then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_string(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 70: map_string_bytes + v = t.map_string_bytes + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xb2\x04", "\x0a", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= '' then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_bytes(_val) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 71: map_string_nested_message + v = t.map_string_nested_message + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xba\x04", "\x0a", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= nil then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_len(M.TestAllTypesProto3_NestedMessage_encode(_val)) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 72: map_string_foreign_message + v = t.map_string_foreign_message + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xc2\x04", "\x0a", "\x12" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + if _val ~= nil then + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_len(M.ForeignMessage_encode(_val)) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 73: map_string_nested_enum + v = t.map_string_nested_enum + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xca\x04", "\x0a", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + do + local _nv = _val + if type(_val) == 'string' then + _nv = M.TestAllTypesProto3_NestedEnum[_val] + if _nv == nil then error("unknown enum value '" .. _val .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_int32(_nv) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 74: map_string_foreign_enum + v = t.map_string_foreign_enum + if v ~= nil and next(v) ~= nil then + local _tag, _ktag, _vtag = "\xd2\x04", "\x0a", "\x10" + for _k, _val in pairs(v) do + local entry, _m = {}, 0 + if _k ~= '' then + _m = _m + 1; entry[_m] = _ktag + _m = _m + 1; entry[_m] = wire.encode_string(_k) + end + do + local _nv = _val + if type(_val) == 'string' then + _nv = M.ForeignEnum[_val] + if _nv == nil then error("unknown enum value '" .. _val .. "' for protobuf_test_messages.proto3.ForeignEnum", 0) end + end + _m = _m + 1; entry[_m] = _vtag + _m = _m + 1; entry[_m] = wire.encode_int32(_nv) + end + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(table.concat(entry)) + end + end + -- field 111: oneof_uint32 + v = t.oneof_uint32 + if _of_oneof_field == "oneof_uint32" then + n = n + 1; out[n] = "\xf8\x06" + n = n + 1; out[n] = wire.encode_uint32(v) + end + -- field 112: oneof_nested_message + v = t.oneof_nested_message + if _of_oneof_field == "oneof_nested_message" then + n = n + 1; out[n] = "\x82\x07" + n = n + 1; out[n] = wire.encode_len(M.TestAllTypesProto3_NestedMessage_encode(v)) + end + -- field 113: oneof_string + v = t.oneof_string + if _of_oneof_field == "oneof_string" then + n = n + 1; out[n] = "\x8a\x07" + n = n + 1; out[n] = wire.encode_string(v) + end + -- field 114: oneof_bytes + v = t.oneof_bytes + if _of_oneof_field == "oneof_bytes" then + n = n + 1; out[n] = "\x92\x07" + n = n + 1; out[n] = wire.encode_bytes(v) + end + -- field 115: oneof_bool + v = t.oneof_bool + if _of_oneof_field == "oneof_bool" then + n = n + 1; out[n] = "\x98\x07" + n = n + 1; out[n] = wire.encode_bool(v) + end + -- field 116: oneof_uint64 + v = t.oneof_uint64 + if _of_oneof_field == "oneof_uint64" then + n = n + 1; out[n] = "\xa0\x07" + n = n + 1; out[n] = wire.encode_uint64(v) + end + -- field 117: oneof_float + v = t.oneof_float + if _of_oneof_field == "oneof_float" then + n = n + 1; out[n] = "\xad\x07" + n = n + 1; out[n] = wire.encode_float(v) + end + -- field 118: oneof_double + v = t.oneof_double + if _of_oneof_field == "oneof_double" then + n = n + 1; out[n] = "\xb1\x07" + n = n + 1; out[n] = wire.encode_double(v) + end + -- field 119: oneof_enum + v = t.oneof_enum + if _of_oneof_field == "oneof_enum" then + local nv = v + if type(v) == 'string' then + nv = M.TestAllTypesProto3_NestedEnum[v] + if nv == nil then error("unknown enum value '" .. v .. "' for protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", 0) end + end + n = n + 1; out[n] = "\xb8\x07" + n = n + 1; out[n] = wire.encode_int32(nv) + end + -- field 120: oneof_null_value + v = t.oneof_null_value + if _of_oneof_field == "oneof_null_value" then + local nv = v + if type(v) == 'string' then + nv = pb.wkt.NullValue[v] + if nv == nil then error("unknown enum value '" .. v .. "' for google.protobuf.NullValue", 0) end + end + n = n + 1; out[n] = "\xc0\x07" + n = n + 1; out[n] = wire.encode_int32(nv) + end + -- field 201: optional_bool_wrapper + v = t.optional_bool_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xca\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.BoolValue_encode(v)) + end + -- field 202: optional_int32_wrapper + v = t.optional_int32_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xd2\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Int32Value_encode(v)) + end + -- field 203: optional_int64_wrapper + v = t.optional_int64_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xda\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Int64Value_encode(v)) + end + -- field 204: optional_uint32_wrapper + v = t.optional_uint32_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xe2\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.UInt32Value_encode(v)) + end + -- field 205: optional_uint64_wrapper + v = t.optional_uint64_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xea\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.UInt64Value_encode(v)) + end + -- field 206: optional_float_wrapper + v = t.optional_float_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xf2\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.FloatValue_encode(v)) + end + -- field 207: optional_double_wrapper + v = t.optional_double_wrapper + if v ~= nil then + n = n + 1; out[n] = "\xfa\x0c" + n = n + 1; out[n] = wire.encode_len(pb.wkt.DoubleValue_encode(v)) + end + -- field 208: optional_string_wrapper + v = t.optional_string_wrapper + if v ~= nil then + n = n + 1; out[n] = "\x82\x0d" + n = n + 1; out[n] = wire.encode_len(pb.wkt.StringValue_encode(v)) + end + -- field 209: optional_bytes_wrapper + v = t.optional_bytes_wrapper + if v ~= nil then + n = n + 1; out[n] = "\x8a\x0d" + n = n + 1; out[n] = wire.encode_len(pb.wkt.BytesValue_encode(v)) + end + -- field 211: repeated_bool_wrapper + v = t.repeated_bool_wrapper + if v ~= nil and #v > 0 then + local _tag = "\x9a\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.BoolValue_encode(v[_i])) + end + end + -- field 212: repeated_int32_wrapper + v = t.repeated_int32_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xa2\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Int32Value_encode(v[_i])) + end + end + -- field 213: repeated_int64_wrapper + v = t.repeated_int64_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xaa\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Int64Value_encode(v[_i])) + end + end + -- field 214: repeated_uint32_wrapper + v = t.repeated_uint32_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xb2\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.UInt32Value_encode(v[_i])) + end + end + -- field 215: repeated_uint64_wrapper + v = t.repeated_uint64_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xba\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.UInt64Value_encode(v[_i])) + end + end + -- field 216: repeated_float_wrapper + v = t.repeated_float_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xc2\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.FloatValue_encode(v[_i])) + end + end + -- field 217: repeated_double_wrapper + v = t.repeated_double_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xca\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.DoubleValue_encode(v[_i])) + end + end + -- field 218: repeated_string_wrapper + v = t.repeated_string_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xd2\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.StringValue_encode(v[_i])) + end + end + -- field 219: repeated_bytes_wrapper + v = t.repeated_bytes_wrapper + if v ~= nil and #v > 0 then + local _tag = "\xda\x0d" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.BytesValue_encode(v[_i])) + end + end + -- field 301: optional_duration + v = t.optional_duration + if v ~= nil then + n = n + 1; out[n] = "\xea\x12" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Duration_encode(v)) + end + -- field 302: optional_timestamp + v = t.optional_timestamp + if v ~= nil then + n = n + 1; out[n] = "\xf2\x12" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Timestamp_encode(v)) + end + -- field 303: optional_field_mask + v = t.optional_field_mask + if v ~= nil then + n = n + 1; out[n] = "\xfa\x12" + n = n + 1; out[n] = wire.encode_len(pb.wkt.FieldMask_encode(v)) + end + -- field 304: optional_struct + v = t.optional_struct + if v ~= nil then + n = n + 1; out[n] = "\x82\x13" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Struct_encode(v)) + end + -- field 305: optional_any + v = t.optional_any + if v ~= nil then + n = n + 1; out[n] = "\x8a\x13" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Any_encode(v)) + end + -- field 306: optional_value + v = t.optional_value + if v ~= nil then + n = n + 1; out[n] = "\x92\x13" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Value_encode(v)) + end + -- field 307: optional_null_value + v = t.optional_null_value + if v ~= nil then + local nv = v + if type(v) == 'string' then + nv = pb.wkt.NullValue[v] + if nv == nil then error("unknown enum value '" .. v .. "' for google.protobuf.NullValue", 0) end + end + if nv ~= 0 then + n = n + 1; out[n] = "\x98\x13" + n = n + 1; out[n] = wire.encode_int32(nv) + end + end + -- field 308: optional_empty + v = t.optional_empty + if v ~= nil then + n = n + 1; out[n] = "\xa2\x13" + n = n + 1; out[n] = wire.encode_len(pb.wkt.Empty_encode(v)) + end + -- field 311: repeated_duration + v = t.repeated_duration + if v ~= nil and #v > 0 then + local _tag = "\xba\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Duration_encode(v[_i])) + end + end + -- field 312: repeated_timestamp + v = t.repeated_timestamp + if v ~= nil and #v > 0 then + local _tag = "\xc2\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Timestamp_encode(v[_i])) + end + end + -- field 313: repeated_fieldmask + v = t.repeated_fieldmask + if v ~= nil and #v > 0 then + local _tag = "\xca\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.FieldMask_encode(v[_i])) + end + end + -- field 324: repeated_struct + v = t.repeated_struct + if v ~= nil and #v > 0 then + local _tag = "\xa2\x14" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Struct_encode(v[_i])) + end + end + -- field 315: repeated_any + v = t.repeated_any + if v ~= nil and #v > 0 then + local _tag = "\xda\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Any_encode(v[_i])) + end + end + -- field 316: repeated_value + v = t.repeated_value + if v ~= nil and #v > 0 then + local _tag = "\xe2\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Value_encode(v[_i])) + end + end + -- field 317: repeated_list_value + v = t.repeated_list_value + if v ~= nil and #v > 0 then + local _tag = "\xea\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.ListValue_encode(v[_i])) + end + end + -- field 318: repeated_empty + v = t.repeated_empty + if v ~= nil and #v > 0 then + local _tag = "\xf2\x13" + for _i = 1, #v do + n = n + 1; out[n] = _tag + n = n + 1; out[n] = wire.encode_len(pb.wkt.Empty_encode(v[_i])) + end + end + -- field 401: fieldname1 + v = t.fieldname1 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x88\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 402: field_name2 + v = t.field_name2 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x90\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 403: _field_name3 + v = t._field_name3 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x98\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 404: field__name4_ + v = t.field__name4_ + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xa0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 405: field0name5 + v = t.field0name5 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xa8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 406: field_0_name6 + v = t.field_0_name6 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xb0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 407: fieldName7 + v = t.fieldName7 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xb8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 408: FieldName8 + v = t.FieldName8 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xc0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 409: field_Name9 + v = t.field_Name9 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xc8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 410: Field_Name10 + v = t.Field_Name10 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xd0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 411: FIELD_NAME11 + v = t.FIELD_NAME11 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xd8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 412: FIELD_name12 + v = t.FIELD_name12 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xe0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 413: __field_name13 + v = t.__field_name13 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xe8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 414: __Field_name14 + v = t.__Field_name14 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xf0\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 415: field__name15 + v = t.field__name15 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\xf8\x19" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 416: field__Name16 + v = t.field__Name16 + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x80\x1a" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 417: field_name17__ + v = t.field_name17__ + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x88\x1a" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 418: Field_name18__ + v = t.Field_name18__ + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x90\x1a" + n = n + 1; out[n] = wire.encode_int32(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.TestAllTypesProto3_decode(buf) + if type(buf) ~= 'string' then + error("expected string for protobuf_test_messages.proto3.TestAllTypesProto3 decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_int32(buf, pos) + result.optional_int32 = val + elseif id == 2 then + local val + val, pos = wire.decode_int64(buf, pos) + result.optional_int64 = val + elseif id == 3 then + local val + val, pos = wire.decode_uint32(buf, pos) + result.optional_uint32 = val + elseif id == 4 then + local val + val, pos = wire.decode_uint64(buf, pos) + result.optional_uint64 = val + elseif id == 5 then + local val + val, pos = wire.decode_sint32(buf, pos) + result.optional_sint32 = val + elseif id == 6 then + local val + val, pos = wire.decode_sint64(buf, pos) + result.optional_sint64 = val + elseif id == 7 then + local val + val, pos = wire.decode_fixed32(buf, pos) + result.optional_fixed32 = val + elseif id == 8 then + local val + val, pos = wire.decode_fixed64(buf, pos) + result.optional_fixed64 = val + elseif id == 9 then + local val + val, pos = wire.decode_sfixed32(buf, pos) + result.optional_sfixed32 = val + elseif id == 10 then + local val + val, pos = wire.decode_sfixed64(buf, pos) + result.optional_sfixed64 = val + elseif id == 11 then + local val + val, pos = wire.decode_float(buf, pos) + result.optional_float = val + elseif id == 12 then + local val + val, pos = wire.decode_double(buf, pos) + result.optional_double = val + elseif id == 13 then + local val + val, pos = wire.decode_bool(buf, pos) + result.optional_bool = val + elseif id == 14 then + local val + val, pos = wire.decode_string(buf, pos) + result.optional_string = val + elseif id == 15 then + local val + val, pos = wire.decode_bytes(buf, pos) + result.optional_bytes = val + elseif id == 18 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_nested_message + if prev == nil then + result.optional_nested_message = M.TestAllTypesProto3_NestedMessage_decode(payload) + else + local new = M.TestAllTypesProto3_NestedMessage_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 19 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_foreign_message + if prev == nil then + result.optional_foreign_message = M.ForeignMessage_decode(payload) + else + local new = M.ForeignMessage_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 21 then + local u + u, pos = wire.decode_varint(buf, pos) + result.optional_nested_enum = tonumber(u) + elseif id == 22 then + local u + u, pos = wire.decode_varint(buf, pos) + result.optional_foreign_enum = tonumber(u) + elseif id == 23 then + local u + u, pos = wire.decode_varint(buf, pos) + result.optional_aliased_enum = tonumber(u) + elseif id == 24 then + local val + val, pos = wire.decode_string(buf, pos) + result.optional_string_piece = val + elseif id == 25 then + local val + val, pos = wire.decode_string(buf, pos) + result.optional_cord = val + elseif id == 27 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.recursive_message + if prev == nil then + result.recursive_message = M.TestAllTypesProto3_decode(payload) + else + local new = M.TestAllTypesProto3_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 31 then + local list = result.repeated_int32 + if list == nil then list = {}; result.repeated_int32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int32(buf, pos) + list[#list + 1] = val + end + elseif id == 32 then + local list = result.repeated_int64 + if list == nil then list = {}; result.repeated_int64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int64(buf, pos) + list[#list + 1] = val + end + elseif id == 33 then + local list = result.repeated_uint32 + if list == nil then list = {}; result.repeated_uint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint32(buf, pos) + list[#list + 1] = val + end + elseif id == 34 then + local list = result.repeated_uint64 + if list == nil then list = {}; result.repeated_uint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint64(buf, pos) + list[#list + 1] = val + end + elseif id == 35 then + local list = result.repeated_sint32 + if list == nil then list = {}; result.repeated_sint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint32(buf, pos) + list[#list + 1] = val + end + elseif id == 36 then + local list = result.repeated_sint64 + if list == nil then list = {}; result.repeated_sint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint64(buf, pos) + list[#list + 1] = val + end + elseif id == 37 then + local list = result.repeated_fixed32 + if list == nil then list = {}; result.repeated_fixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 38 then + local list = result.repeated_fixed64 + if list == nil then list = {}; result.repeated_fixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 39 then + local list = result.repeated_sfixed32 + if list == nil then list = {}; result.repeated_sfixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 40 then + local list = result.repeated_sfixed64 + if list == nil then list = {}; result.repeated_sfixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 41 then + local list = result.repeated_float + if list == nil then list = {}; result.repeated_float = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_float(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_float(buf, pos) + list[#list + 1] = val + end + elseif id == 42 then + local list = result.repeated_double + if list == nil then list = {}; result.repeated_double = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_double(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_double(buf, pos) + list[#list + 1] = val + end + elseif id == 43 then + local list = result.repeated_bool + if list == nil then list = {}; result.repeated_bool = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_bool(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_bool(buf, pos) + list[#list + 1] = val + end + elseif id == 44 then + local list = result.repeated_string + if list == nil then list = {}; result.repeated_string = list end + local val + val, pos = wire.decode_string(buf, pos) + list[#list + 1] = val + elseif id == 45 then + local list = result.repeated_bytes + if list == nil then list = {}; result.repeated_bytes = list end + local val + val, pos = wire.decode_bytes(buf, pos) + list[#list + 1] = val + elseif id == 48 then + local list = result.repeated_nested_message + if list == nil then list = {}; result.repeated_nested_message = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = M.TestAllTypesProto3_NestedMessage_decode(payload) + elseif id == 49 then + local list = result.repeated_foreign_message + if list == nil then list = {}; result.repeated_foreign_message = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = M.ForeignMessage_decode(payload) + elseif id == 51 then + local list = result.repeated_nested_enum + if list == nil then list = {}; result.repeated_nested_enum = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local u + u, p2 = wire.decode_varint(payload, p2) + list[#list + 1] = tonumber(u) + end + else + local u + u, pos = wire.decode_varint(buf, pos) + list[#list + 1] = tonumber(u) + end + elseif id == 52 then + local list = result.repeated_foreign_enum + if list == nil then list = {}; result.repeated_foreign_enum = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local u + u, p2 = wire.decode_varint(payload, p2) + list[#list + 1] = tonumber(u) + end + else + local u + u, pos = wire.decode_varint(buf, pos) + list[#list + 1] = tonumber(u) + end + elseif id == 54 then + local list = result.repeated_string_piece + if list == nil then list = {}; result.repeated_string_piece = list end + local val + val, pos = wire.decode_string(buf, pos) + list[#list + 1] = val + elseif id == 55 then + local list = result.repeated_cord + if list == nil then list = {}; result.repeated_cord = list end + local val + val, pos = wire.decode_string(buf, pos) + list[#list + 1] = val + elseif id == 75 then + local list = result.packed_int32 + if list == nil then list = {}; result.packed_int32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int32(buf, pos) + list[#list + 1] = val + end + elseif id == 76 then + local list = result.packed_int64 + if list == nil then list = {}; result.packed_int64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int64(buf, pos) + list[#list + 1] = val + end + elseif id == 77 then + local list = result.packed_uint32 + if list == nil then list = {}; result.packed_uint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint32(buf, pos) + list[#list + 1] = val + end + elseif id == 78 then + local list = result.packed_uint64 + if list == nil then list = {}; result.packed_uint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint64(buf, pos) + list[#list + 1] = val + end + elseif id == 79 then + local list = result.packed_sint32 + if list == nil then list = {}; result.packed_sint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint32(buf, pos) + list[#list + 1] = val + end + elseif id == 80 then + local list = result.packed_sint64 + if list == nil then list = {}; result.packed_sint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint64(buf, pos) + list[#list + 1] = val + end + elseif id == 81 then + local list = result.packed_fixed32 + if list == nil then list = {}; result.packed_fixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 82 then + local list = result.packed_fixed64 + if list == nil then list = {}; result.packed_fixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 83 then + local list = result.packed_sfixed32 + if list == nil then list = {}; result.packed_sfixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 84 then + local list = result.packed_sfixed64 + if list == nil then list = {}; result.packed_sfixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 85 then + local list = result.packed_float + if list == nil then list = {}; result.packed_float = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_float(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_float(buf, pos) + list[#list + 1] = val + end + elseif id == 86 then + local list = result.packed_double + if list == nil then list = {}; result.packed_double = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_double(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_double(buf, pos) + list[#list + 1] = val + end + elseif id == 87 then + local list = result.packed_bool + if list == nil then list = {}; result.packed_bool = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_bool(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_bool(buf, pos) + list[#list + 1] = val + end + elseif id == 88 then + local list = result.packed_nested_enum + if list == nil then list = {}; result.packed_nested_enum = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local u + u, p2 = wire.decode_varint(payload, p2) + list[#list + 1] = tonumber(u) + end + else + local u + u, pos = wire.decode_varint(buf, pos) + list[#list + 1] = tonumber(u) + end + elseif id == 89 then + local list = result.unpacked_int32 + if list == nil then list = {}; result.unpacked_int32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int32(buf, pos) + list[#list + 1] = val + end + elseif id == 90 then + local list = result.unpacked_int64 + if list == nil then list = {}; result.unpacked_int64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_int64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_int64(buf, pos) + list[#list + 1] = val + end + elseif id == 91 then + local list = result.unpacked_uint32 + if list == nil then list = {}; result.unpacked_uint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint32(buf, pos) + list[#list + 1] = val + end + elseif id == 92 then + local list = result.unpacked_uint64 + if list == nil then list = {}; result.unpacked_uint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_uint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_uint64(buf, pos) + list[#list + 1] = val + end + elseif id == 93 then + local list = result.unpacked_sint32 + if list == nil then list = {}; result.unpacked_sint32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint32(buf, pos) + list[#list + 1] = val + end + elseif id == 94 then + local list = result.unpacked_sint64 + if list == nil then list = {}; result.unpacked_sint64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sint64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sint64(buf, pos) + list[#list + 1] = val + end + elseif id == 95 then + local list = result.unpacked_fixed32 + if list == nil then list = {}; result.unpacked_fixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 96 then + local list = result.unpacked_fixed64 + if list == nil then list = {}; result.unpacked_fixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_fixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_fixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 97 then + local list = result.unpacked_sfixed32 + if list == nil then list = {}; result.unpacked_sfixed32 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed32(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed32(buf, pos) + list[#list + 1] = val + end + elseif id == 98 then + local list = result.unpacked_sfixed64 + if list == nil then list = {}; result.unpacked_sfixed64 = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_sfixed64(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_sfixed64(buf, pos) + list[#list + 1] = val + end + elseif id == 99 then + local list = result.unpacked_float + if list == nil then list = {}; result.unpacked_float = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_float(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_float(buf, pos) + list[#list + 1] = val + end + elseif id == 100 then + local list = result.unpacked_double + if list == nil then list = {}; result.unpacked_double = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_double(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_double(buf, pos) + list[#list + 1] = val + end + elseif id == 101 then + local list = result.unpacked_bool + if list == nil then list = {}; result.unpacked_bool = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local val + val, p2 = wire.decode_bool(payload, p2) + list[#list + 1] = val + end + else + local val + val, pos = wire.decode_bool(buf, pos) + list[#list + 1] = val + end + elseif id == 102 then + local list = result.unpacked_nested_enum + if list == nil then list = {}; result.unpacked_nested_enum = list end + if wt == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local p2, lim = 1, #payload + while p2 <= lim do + local u + u, p2 = wire.decode_varint(payload, p2) + list[#list + 1] = tonumber(u) + end + else + local u + u, pos = wire.decode_varint(buf, pos) + list[#list + 1] = tonumber(u) + end + elseif id == 56 then + local map = result.map_int32_int32 + if map == nil then map = {}; result.map_int32_int32 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_int32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_int32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 57 then + local map = result.map_int64_int64 + if map == nil then map = {}; result.map_int64_int64 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_int64(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_int64(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 58 then + local map = result.map_uint32_uint32 + if map == nil then map = {}; result.map_uint32_uint32 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_uint32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_uint32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 59 then + local map = result.map_uint64_uint64 + if map == nil then map = {}; result.map_uint64_uint64 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_uint64(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_uint64(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 60 then + local map = result.map_sint32_sint32 + if map == nil then map = {}; result.map_sint32_sint32 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_sint32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_sint32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 61 then + local map = result.map_sint64_sint64 + if map == nil then map = {}; result.map_sint64_sint64 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_sint64(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_sint64(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 62 then + local map = result.map_fixed32_fixed32 + if map == nil then map = {}; result.map_fixed32_fixed32 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_fixed32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_fixed32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 63 then + local map = result.map_fixed64_fixed64 + if map == nil then map = {}; result.map_fixed64_fixed64 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_fixed64(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_fixed64(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 64 then + local map = result.map_sfixed32_sfixed32 + if map == nil then map = {}; result.map_sfixed32_sfixed32 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_sfixed32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_sfixed32(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 65 then + local map = result.map_sfixed64_sfixed64 + if map == nil then map = {}; result.map_sfixed64_sfixed64 = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_sfixed64(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_sfixed64(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 66 then + local map = result.map_int32_float + if map == nil then map = {}; result.map_int32_float = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_int32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_float(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 67 then + local map = result.map_int32_double + if map == nil then map = {}; result.map_int32_double = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = 0, 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_int32(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_double(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 68 then + local map = result.map_bool_bool + if map == nil then map = {}; result.map_bool_bool = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = false, false + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_bool(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_bool(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 69 then + local map = result.map_string_string + if map == nil then map = {}; result.map_string_string = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', '' + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_string(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 70 then + local map = result.map_string_bytes + if map == nil then map = {}; result.map_string_bytes = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', '' + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + _val, _ep = wire.decode_bytes(payload, _ep) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 71 then + local map = result.map_string_nested_message + if map == nil then map = {}; result.map_string_nested_message = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', {} + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + local _payload + _payload, _ep = wire.decode_len(payload, _ep) + _val = M.TestAllTypesProto3_NestedMessage_decode(_payload) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 72 then + local map = result.map_string_foreign_message + if map == nil then map = {}; result.map_string_foreign_message = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', {} + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + local _payload + _payload, _ep = wire.decode_len(payload, _ep) + _val = M.ForeignMessage_decode(_payload) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 73 then + local map = result.map_string_nested_enum + if map == nil then map = {}; result.map_string_nested_enum = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + local _u + _u, _ep = wire.decode_varint(payload, _ep) + _val = tonumber(_u) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 74 then + local map = result.map_string_foreign_enum + if map == nil then map = {}; result.map_string_foreign_enum = map end + local payload + payload, pos = wire.decode_len(buf, pos) + local _ep, _elim = 1, #payload + local _key, _val = '', 0 + while _ep <= _elim do + local eid, ewt + eid, ewt, _ep = wire.decode_tag(payload, _ep) + if eid == 1 then + _key, _ep = wire.decode_string(payload, _ep) + elseif eid == 2 then + local _u + _u, _ep = wire.decode_varint(payload, _ep) + _val = tonumber(_u) + else + _ep = wire.skip_field(payload, _ep, ewt) + end + end + map[_key] = _val + elseif id == 111 then + local val + val, pos = wire.decode_uint32(buf, pos) + result.oneof_uint32 = val + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 112 then + local payload + payload, pos = wire.decode_len(buf, pos) + result.oneof_nested_message = M.TestAllTypesProto3_NestedMessage_decode(payload) + result.oneof_uint32 = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 113 then + local val + val, pos = wire.decode_string(buf, pos) + result.oneof_string = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 114 then + local val + val, pos = wire.decode_bytes(buf, pos) + result.oneof_bytes = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 115 then + local val + val, pos = wire.decode_bool(buf, pos) + result.oneof_bool = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 116 then + local val + val, pos = wire.decode_uint64(buf, pos) + result.oneof_uint64 = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 117 then + local val + val, pos = wire.decode_float(buf, pos) + result.oneof_float = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_double = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 118 then + local val + val, pos = wire.decode_double(buf, pos) + result.oneof_double = val + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_enum = nil + result.oneof_null_value = nil + elseif id == 119 then + local u + u, pos = wire.decode_varint(buf, pos) + result.oneof_enum = tonumber(u) + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_null_value = nil + elseif id == 120 then + local u + u, pos = wire.decode_varint(buf, pos) + result.oneof_null_value = tonumber(u) + result.oneof_uint32 = nil + result.oneof_nested_message = nil + result.oneof_string = nil + result.oneof_bytes = nil + result.oneof_bool = nil + result.oneof_uint64 = nil + result.oneof_float = nil + result.oneof_double = nil + result.oneof_enum = nil + elseif id == 201 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_bool_wrapper + if prev == nil then + result.optional_bool_wrapper = pb.wkt.BoolValue_decode(payload) + else + local new = pb.wkt.BoolValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 202 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_int32_wrapper + if prev == nil then + result.optional_int32_wrapper = pb.wkt.Int32Value_decode(payload) + else + local new = pb.wkt.Int32Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 203 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_int64_wrapper + if prev == nil then + result.optional_int64_wrapper = pb.wkt.Int64Value_decode(payload) + else + local new = pb.wkt.Int64Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 204 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_uint32_wrapper + if prev == nil then + result.optional_uint32_wrapper = pb.wkt.UInt32Value_decode(payload) + else + local new = pb.wkt.UInt32Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 205 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_uint64_wrapper + if prev == nil then + result.optional_uint64_wrapper = pb.wkt.UInt64Value_decode(payload) + else + local new = pb.wkt.UInt64Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 206 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_float_wrapper + if prev == nil then + result.optional_float_wrapper = pb.wkt.FloatValue_decode(payload) + else + local new = pb.wkt.FloatValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 207 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_double_wrapper + if prev == nil then + result.optional_double_wrapper = pb.wkt.DoubleValue_decode(payload) + else + local new = pb.wkt.DoubleValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 208 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_string_wrapper + if prev == nil then + result.optional_string_wrapper = pb.wkt.StringValue_decode(payload) + else + local new = pb.wkt.StringValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 209 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_bytes_wrapper + if prev == nil then + result.optional_bytes_wrapper = pb.wkt.BytesValue_decode(payload) + else + local new = pb.wkt.BytesValue_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 211 then + local list = result.repeated_bool_wrapper + if list == nil then list = {}; result.repeated_bool_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.BoolValue_decode(payload) + elseif id == 212 then + local list = result.repeated_int32_wrapper + if list == nil then list = {}; result.repeated_int32_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Int32Value_decode(payload) + elseif id == 213 then + local list = result.repeated_int64_wrapper + if list == nil then list = {}; result.repeated_int64_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Int64Value_decode(payload) + elseif id == 214 then + local list = result.repeated_uint32_wrapper + if list == nil then list = {}; result.repeated_uint32_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.UInt32Value_decode(payload) + elseif id == 215 then + local list = result.repeated_uint64_wrapper + if list == nil then list = {}; result.repeated_uint64_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.UInt64Value_decode(payload) + elseif id == 216 then + local list = result.repeated_float_wrapper + if list == nil then list = {}; result.repeated_float_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.FloatValue_decode(payload) + elseif id == 217 then + local list = result.repeated_double_wrapper + if list == nil then list = {}; result.repeated_double_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.DoubleValue_decode(payload) + elseif id == 218 then + local list = result.repeated_string_wrapper + if list == nil then list = {}; result.repeated_string_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.StringValue_decode(payload) + elseif id == 219 then + local list = result.repeated_bytes_wrapper + if list == nil then list = {}; result.repeated_bytes_wrapper = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.BytesValue_decode(payload) + elseif id == 301 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_duration + if prev == nil then + result.optional_duration = pb.wkt.Duration_decode(payload) + else + local new = pb.wkt.Duration_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 302 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_timestamp + if prev == nil then + result.optional_timestamp = pb.wkt.Timestamp_decode(payload) + else + local new = pb.wkt.Timestamp_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 303 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_field_mask + if prev == nil then + result.optional_field_mask = pb.wkt.FieldMask_decode(payload) + else + local new = pb.wkt.FieldMask_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 304 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_struct + if prev == nil then + result.optional_struct = pb.wkt.Struct_decode(payload) + else + local new = pb.wkt.Struct_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 305 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_any + if prev == nil then + result.optional_any = pb.wkt.Any_decode(payload) + else + local new = pb.wkt.Any_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 306 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_value + if prev == nil then + result.optional_value = pb.wkt.Value_decode(payload) + else + local new = pb.wkt.Value_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 307 then + local u + u, pos = wire.decode_varint(buf, pos) + result.optional_null_value = tonumber(u) + elseif id == 308 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.optional_empty + if prev == nil then + result.optional_empty = pb.wkt.Empty_decode(payload) + else + local new = pb.wkt.Empty_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + elseif id == 311 then + local list = result.repeated_duration + if list == nil then list = {}; result.repeated_duration = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Duration_decode(payload) + elseif id == 312 then + local list = result.repeated_timestamp + if list == nil then list = {}; result.repeated_timestamp = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Timestamp_decode(payload) + elseif id == 313 then + local list = result.repeated_fieldmask + if list == nil then list = {}; result.repeated_fieldmask = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.FieldMask_decode(payload) + elseif id == 324 then + local list = result.repeated_struct + if list == nil then list = {}; result.repeated_struct = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Struct_decode(payload) + elseif id == 315 then + local list = result.repeated_any + if list == nil then list = {}; result.repeated_any = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Any_decode(payload) + elseif id == 316 then + local list = result.repeated_value + if list == nil then list = {}; result.repeated_value = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Value_decode(payload) + elseif id == 317 then + local list = result.repeated_list_value + if list == nil then list = {}; result.repeated_list_value = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.ListValue_decode(payload) + elseif id == 318 then + local list = result.repeated_empty + if list == nil then list = {}; result.repeated_empty = list end + local payload + payload, pos = wire.decode_len(buf, pos) + list[#list + 1] = pb.wkt.Empty_decode(payload) + elseif id == 401 then + local val + val, pos = wire.decode_int32(buf, pos) + result.fieldname1 = val + elseif id == 402 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field_name2 = val + elseif id == 403 then + local val + val, pos = wire.decode_int32(buf, pos) + result._field_name3 = val + elseif id == 404 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field__name4_ = val + elseif id == 405 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field0name5 = val + elseif id == 406 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field_0_name6 = val + elseif id == 407 then + local val + val, pos = wire.decode_int32(buf, pos) + result.fieldName7 = val + elseif id == 408 then + local val + val, pos = wire.decode_int32(buf, pos) + result.FieldName8 = val + elseif id == 409 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field_Name9 = val + elseif id == 410 then + local val + val, pos = wire.decode_int32(buf, pos) + result.Field_Name10 = val + elseif id == 411 then + local val + val, pos = wire.decode_int32(buf, pos) + result.FIELD_NAME11 = val + elseif id == 412 then + local val + val, pos = wire.decode_int32(buf, pos) + result.FIELD_name12 = val + elseif id == 413 then + local val + val, pos = wire.decode_int32(buf, pos) + result.__field_name13 = val + elseif id == 414 then + local val + val, pos = wire.decode_int32(buf, pos) + result.__Field_name14 = val + elseif id == 415 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field__name15 = val + elseif id == 416 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field__Name16 = val + elseif id == 417 then + local val + val, pos = wire.decode_int32(buf, pos) + result.field_name17__ = val + elseif id == 418 then + local val + val, pos = wire.decode_int32(buf, pos) + result.Field_name18__ = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.TestAllTypesProto3_NestedMessage_new(t) return t or {} end + +function M.TestAllTypesProto3_NestedMessage_encode(t) + if type(t) ~= 'table' then + error("expected table for protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: a + v = t.a + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_int32(v) + end + -- field 2: corecursive + v = t.corecursive + if v ~= nil then + n = n + 1; out[n] = "\x12" + n = n + 1; out[n] = wire.encode_len(M.TestAllTypesProto3_encode(v)) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.TestAllTypesProto3_NestedMessage_decode(buf) + if type(buf) ~= 'string' then + error("expected string for protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_int32(buf, pos) + result.a = val + elseif id == 2 then + local payload + payload, pos = wire.decode_len(buf, pos) + local prev = result.corecursive + if prev == nil then + result.corecursive = M.TestAllTypesProto3_decode(payload) + else + local new = M.TestAllTypesProto3_decode(payload) + for k, val in pairs(new) do prev[k] = val end + end + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.ForeignMessage_new(t) return t or {} end + +function M.ForeignMessage_encode(t) + if type(t) ~= 'table' then + error("expected table for protobuf_test_messages.proto3.ForeignMessage, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + -- field 1: c + v = t.c + if v ~= nil and v ~= 0 then + n = n + 1; out[n] = "\x08" + n = n + 1; out[n] = wire.encode_int32(v) + end + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.ForeignMessage_decode(buf) + if type(buf) ~= 'string' then + error("expected string for protobuf_test_messages.proto3.ForeignMessage decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local val + val, pos = wire.decode_int32(buf, pos) + result.c = val + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.NullHypothesisProto3_new(t) return t or {} end + +function M.NullHypothesisProto3_encode(t) + if type(t) ~= 'table' then + error("expected table for protobuf_test_messages.proto3.NullHypothesisProto3, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.NullHypothesisProto3_decode(buf) + if type(buf) ~= 'string' then + error("expected string for protobuf_test_messages.proto3.NullHypothesisProto3 decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if true then + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +function M.EnumOnlyProto3_new(t) return t or {} end + +function M.EnumOnlyProto3_encode(t) + if type(t) ~= 'table' then + error("expected table for protobuf_test_messages.proto3.EnumOnlyProto3, got " .. type(t), 0) + end + local out, n = {}, 0 + local v + local _uf = t._unknown_fields + if _uf ~= nil and _uf ~= '' then n = n + 1; out[n] = _uf end + return table.concat(out) +end + +function M.EnumOnlyProto3_decode(buf) + if type(buf) ~= 'string' then + error("expected string for protobuf_test_messages.proto3.EnumOnlyProto3 decode, got " .. type(buf), 0) + end + local result = {} + local pos, len = 1, #buf + local _uf + while pos <= len do + local _tag_start = pos + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if true then + else + pos = wire.skip_field(buf, pos, wt) + if _uf == nil then _uf = {} end + _uf[#_uf + 1] = buf:sub(_tag_start, pos - 1) + end + end + if _uf ~= nil then result._unknown_fields = table.concat(_uf) end + return result +end + + +return M diff --git a/examples/expected/runtime/conformance/conformance_pb.lua b/examples/expected/runtime/conformance/conformance_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..4175cc2bae749d184eaf4f4f5678481c10928972 --- /dev/null +++ b/examples/expected/runtime/conformance/conformance_pb.lua @@ -0,0 +1,113 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: conformance.proto +-- syntax: proto3 +-- package: conformance + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: conformance.WireFormat +M.WireFormat_descriptor = pb.enum("conformance.WireFormat", { + UNSPECIFIED = 0, + PROTOBUF = 1, + JSON = 2, + JSPB = 3, + TEXT_FORMAT = 4, +}) +M.WireFormat = M.WireFormat_descriptor.by_name + +-- Enum: conformance.TestCategory +M.TestCategory_descriptor = pb.enum("conformance.TestCategory", { + UNSPECIFIED_TEST = 0, + BINARY_TEST = 1, + JSON_TEST = 2, + JSON_IGNORE_UNKNOWN_PARSING_TEST = 3, + JSPB_TEST = 4, + TEXT_FORMAT_TEST = 5, +}) +M.TestCategory = M.TestCategory_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.TestStatus_descriptor = {name = "conformance.TestStatus"} +M.FailureSet_descriptor = {name = "conformance.FailureSet"} +M.ConformanceRequest_descriptor = {name = "conformance.ConformanceRequest"} +M.ConformanceResponse_descriptor = {name = "conformance.ConformanceResponse"} +M.JspbEncodingConfig_descriptor = {name = "conformance.JspbEncodingConfig"} + +-- Message: conformance.TestStatus +M.TestStatus_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, + {name="failure_message", id=2, kind='scalar', proto_type="string"}, + {name="matched_name", id=3, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.TestStatus_descriptor) + +-- Message: conformance.FailureSet +M.FailureSet_descriptor.fields = { + {name="test", id=2, kind='message', message=M.TestStatus_descriptor, repeated=true}, +} +pb.finalize_message(M.FailureSet_descriptor) + +-- Message: conformance.ConformanceRequest +M.ConformanceRequest_descriptor.fields = { + {name="protobuf_payload", id=1, kind='scalar', proto_type="bytes", oneof="payload"}, + {name="json_payload", id=2, kind='scalar', proto_type="string", oneof="payload"}, + {name="jspb_payload", id=7, kind='scalar', proto_type="string", oneof="payload"}, + {name="text_payload", id=8, kind='scalar', proto_type="string", oneof="payload"}, + {name="requested_output_format", id=3, kind='enum', enum=M.WireFormat_descriptor}, + {name="message_type", id=4, kind='scalar', proto_type="string"}, + {name="test_category", id=5, kind='enum', enum=M.TestCategory_descriptor}, + {name="jspb_encoding_options", id=6, kind='message', message=M.JspbEncodingConfig_descriptor}, + {name="print_unknown_fields", id=9, kind='scalar', proto_type="bool"}, +} +M.ConformanceRequest_descriptor.oneofs = { + payload = {"protobuf_payload", "json_payload", "jspb_payload", "text_payload"}, +} +pb.finalize_message(M.ConformanceRequest_descriptor) + +-- Message: conformance.ConformanceResponse +M.ConformanceResponse_descriptor.fields = { + {name="parse_error", id=1, kind='scalar', proto_type="string", oneof="result"}, + {name="serialize_error", id=6, kind='scalar', proto_type="string", oneof="result"}, + {name="timeout_error", id=9, kind='scalar', proto_type="string", oneof="result"}, + {name="runtime_error", id=2, kind='scalar', proto_type="string", oneof="result"}, + {name="protobuf_payload", id=3, kind='scalar', proto_type="bytes", oneof="result"}, + {name="json_payload", id=4, kind='scalar', proto_type="string", oneof="result"}, + {name="skipped", id=5, kind='scalar', proto_type="string", oneof="result"}, + {name="jspb_payload", id=7, kind='scalar', proto_type="string", oneof="result"}, + {name="text_payload", id=8, kind='scalar', proto_type="string", oneof="result"}, +} +M.ConformanceResponse_descriptor.oneofs = { + result = {"parse_error", "serialize_error", "timeout_error", "runtime_error", "protobuf_payload", "json_payload", "skipped", "jspb_payload", "text_payload"}, +} +pb.finalize_message(M.ConformanceResponse_descriptor) + +-- Message: conformance.JspbEncodingConfig +M.JspbEncodingConfig_descriptor.fields = { + {name="use_jspb_array_any_format", id=1, kind='scalar', proto_type="bool"}, +} +pb.finalize_message(M.JspbEncodingConfig_descriptor) + +function M.TestStatus_new(t) return t or {} end +function M.TestStatus_encode(t) return pb.encode(M.TestStatus_descriptor, t) end +function M.TestStatus_decode(b) return pb.decode(M.TestStatus_descriptor, b) end + +function M.FailureSet_new(t) return t or {} end +function M.FailureSet_encode(t) return pb.encode(M.FailureSet_descriptor, t) end +function M.FailureSet_decode(b) return pb.decode(M.FailureSet_descriptor, b) end + +function M.ConformanceRequest_new(t) return t or {} end +function M.ConformanceRequest_encode(t) return pb.encode(M.ConformanceRequest_descriptor, t) end +function M.ConformanceRequest_decode(b) return pb.decode(M.ConformanceRequest_descriptor, b) end + +function M.ConformanceResponse_new(t) return t or {} end +function M.ConformanceResponse_encode(t) return pb.encode(M.ConformanceResponse_descriptor, t) end +function M.ConformanceResponse_decode(b) return pb.decode(M.ConformanceResponse_descriptor, b) end + +function M.JspbEncodingConfig_new(t) return t or {} end +function M.JspbEncodingConfig_encode(t) return pb.encode(M.JspbEncodingConfig_descriptor, t) end +function M.JspbEncodingConfig_decode(b) return pb.decode(M.JspbEncodingConfig_descriptor, b) end + +return M diff --git a/examples/expected/runtime/hello/hello_pb.lua b/examples/expected/runtime/hello/hello_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..38b639e7f43e7acd30758250ac6475f21903f476 --- /dev/null +++ b/examples/expected/runtime/hello/hello_pb.lua @@ -0,0 +1,248 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: hello.proto +-- syntax: proto3 +-- package: hello + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: hello.Status +M.Status_descriptor = pb.enum("hello.Status", { + UNKNOWN = 0, + OK = 1, + ERROR = 2, +}) +M.Status = M.Status_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.Result_descriptor = {name = "hello.Result"} +M.HelloRequest_descriptor = {name = "hello.HelloRequest"} +M.HelloReply_descriptor = {name = "hello.HelloReply"} +M.Event_descriptor = {name = "hello.Event"} +M.Address_descriptor = {name = "hello.Address"} +M.Person_descriptor = {name = "hello.Person"} + +-- Message: hello.Result +M.Result_descriptor.fields = { + {name="id", id=1, kind='scalar', proto_type="int32"}, + {name="text", id=2, kind='scalar', proto_type="string", oneof="outcome"}, + {name="code", id=3, kind='scalar', proto_type="int32", oneof="outcome"}, + {name="details", id=4, kind='message', message=M.Address_descriptor, oneof="outcome"}, +} +M.Result_descriptor.oneofs = { + outcome = {"text", "code", "details"}, +} +pb.finalize_message(M.Result_descriptor) + +-- Message: hello.HelloRequest +M.HelloRequest_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.HelloRequest_descriptor) + +-- Message: hello.HelloReply +M.HelloReply_descriptor.fields = { + {name="greeting", id=1, kind='scalar', proto_type="string"}, +} +pb.finalize_message(M.HelloReply_descriptor) + +-- Message: hello.Event +M.Event_descriptor.fields = { + {name="title", id=1, kind='scalar', proto_type="string"}, + {name="created_at", id=2, kind='message', message=pb.wkt.Timestamp_descriptor}, + {name="duration", id=3, kind='message', message=pb.wkt.Duration_descriptor}, + {name="ack", id=4, kind='message', message=pb.wkt.Empty_descriptor}, + {name="retry_count", id=5, kind='message', message=pb.wkt.Int32Value_descriptor}, + {name="note", id=6, kind='message', message=pb.wkt.StringValue_descriptor}, + {name="is_admin", id=7, kind='message', message=pb.wkt.BoolValue_descriptor}, + {name="payload", id=8, kind='message', message=pb.wkt.Struct_descriptor}, + {name="attribute", id=9, kind='message', message=pb.wkt.Value_descriptor}, + {name="tags", id=10, kind='message', message=pb.wkt.ListValue_descriptor}, + {name="extension", id=11, kind='message', message=pb.wkt.Any_descriptor}, + {name="update_mask", id=12, kind='message', message=pb.wkt.FieldMask_descriptor}, +} +pb.finalize_message(M.Event_descriptor) + +-- Message: hello.Address +M.Address_descriptor.fields = { + {name="street", id=1, kind='scalar', proto_type="string"}, + {name="city", id=2, kind='scalar', proto_type="string"}, + {name="zip", id=3, kind='scalar', proto_type="int32"}, + {name="apartment", id=4, kind='scalar', proto_type="string", optional=true}, +} +pb.finalize_message(M.Address_descriptor) + +-- Message: hello.Person +M.Person_descriptor.fields = { + {name="name", id=1, kind='scalar', proto_type="string"}, + {name="age", id=2, kind='scalar', proto_type="int32"}, + {name="emails", id=3, kind='scalar', proto_type="string", repeated=true}, + {name="status", id=4, kind='enum', enum=M.Status_descriptor}, + {name="address", id=5, kind='message', message=M.Address_descriptor}, + {name="friends", id=6, kind='message', message=M.Person_descriptor, repeated=true}, + {name="lucky_numbers", id=7, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="avatar", id=8, kind='scalar', proto_type="bytes"}, + {name="user_id", id=9, kind='scalar', proto_type="fixed64"}, + {name="balance", id=10, kind='scalar', proto_type="sint32"}, + {name="weight_kg", id=11, kind='scalar', proto_type="double"}, + {name="ages_by_nickname", id=13, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="int32"}}, + {name="nickname_by_age", id=14, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="string"}}, + {name="addresses_by_label", id=15, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.Address_descriptor}}, +} +pb.finalize_message(M.Person_descriptor) + +function M.Result_new(t) return t or {} end +function M.Result_encode(t) return pb.encode(M.Result_descriptor, t) end +function M.Result_decode(b) return pb.decode(M.Result_descriptor, b) end + +function M.HelloRequest_new(t) return t or {} end +function M.HelloRequest_encode(t) return pb.encode(M.HelloRequest_descriptor, t) end +function M.HelloRequest_decode(b) return pb.decode(M.HelloRequest_descriptor, b) end + +function M.HelloReply_new(t) return t or {} end +function M.HelloReply_encode(t) return pb.encode(M.HelloReply_descriptor, t) end +function M.HelloReply_decode(b) return pb.decode(M.HelloReply_descriptor, b) end + +function M.Event_new(t) return t or {} end +function M.Event_encode(t) return pb.encode(M.Event_descriptor, t) end +function M.Event_decode(b) return pb.decode(M.Event_descriptor, b) end + +function M.Address_new(t) return t or {} end +function M.Address_encode(t) return pb.encode(M.Address_descriptor, t) end +function M.Address_decode(b) return pb.decode(M.Address_descriptor, b) end +function M.Address_has_apartment(t) return t.apartment ~= nil end +function M.Address_clear_apartment(t) t.apartment = nil end + +function M.Person_new(t) return t or {} end +function M.Person_encode(t) return pb.encode(M.Person_descriptor, t) end +function M.Person_decode(b) return pb.decode(M.Person_descriptor, b) end + +-- Service: hello.Greeter +M.Greeter_service = { + name = "hello.Greeter", + full_name = "/hello.Greeter", + methods = { + SayHello = { + name = "SayHello", + full_name = "/hello.Greeter/SayHello", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + }, + Echo = { + name = "Echo", + full_name = "/hello.Greeter/Echo", + input = M.HelloRequest_descriptor, + output = M.HelloRequest_descriptor, + }, + StreamHellos = { + name = "StreamHellos", + full_name = "/hello.Greeter/StreamHellos", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + server_streaming = true, + }, + CollectHellos = { + name = "CollectHellos", + full_name = "/hello.Greeter/CollectHellos", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + client_streaming = true, + }, + Chat = { + name = "Chat", + full_name = "/hello.Greeter/Chat", + input = M.HelloRequest_descriptor, + output = M.HelloReply_descriptor, + client_streaming = true, + server_streaming = true, + }, + }, +} + +function M.Greeter_client(transport) + if transport == nil then error("Greeter_client: transport is required", 0) end + return { + SayHello = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local resp_bytes = transport:unary("/hello.Greeter/SayHello", req_bytes, ctx) + return M.HelloReply_decode(resp_bytes) + end, + Echo = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local resp_bytes = transport:unary("/hello.Greeter/Echo", req_bytes, ctx) + return M.HelloRequest_decode(resp_bytes) + end, + StreamHellos = function(req, ctx) + local req_bytes = M.HelloRequest_encode(req) + local raw = transport:server_stream("/hello.Greeter/StreamHellos", req_bytes, ctx) + return pb.grpc.wrap_server_stream(raw, M.HelloReply_decode) + end, + CollectHellos = function(ctx) + local raw = transport:client_stream("/hello.Greeter/CollectHellos", ctx) + return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) + end, + Chat = function(ctx) + local raw = transport:bidi("/hello.Greeter/Chat", ctx) + return pb.grpc.wrap_call(raw, M.HelloRequest_encode, M.HelloReply_decode) + end, + } +end + +function M.Greeter_server(impl) + if type(impl) ~= 'table' then error("Greeter_server: impl table is required", 0) end + return { + service = M.Greeter_service, + methods = { + ["/hello.Greeter/SayHello"] = function(req_bytes, ctx) + local handler = impl.SayHello + if handler == nil then error("Greeter.SayHello: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local resp = handler(req, ctx) + return M.HelloReply_encode(resp) + end, + ["/hello.Greeter/Echo"] = function(req_bytes, ctx) + local handler = impl.Echo + if handler == nil then error("Greeter.Echo: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local resp = handler(req, ctx) + return M.HelloRequest_encode(resp) + end, + }, + streams = { + ["/hello.Greeter/StreamHellos"] = { + kind = 'server_stream', + handler = function(req_bytes, server_view, ctx) + local handler = impl.StreamHellos + if handler == nil then error("Greeter.StreamHellos: handler missing", 0) end + local req = M.HelloRequest_decode(req_bytes) + local wrapped = pb.grpc.wrap_server_view(server_view, nil, M.HelloReply_encode) + handler(req, wrapped, ctx) + end, + }, + ["/hello.Greeter/CollectHellos"] = { + kind = 'client_stream', + handler = function(_, server_view, ctx) + local handler = impl.CollectHellos + if handler == nil then error("Greeter.CollectHellos: handler missing", 0) end + local wrapped = pb.grpc.wrap_server_view(server_view, M.HelloRequest_decode, nil) + local resp = handler(wrapped, ctx) + if resp == nil then error("Greeter.CollectHellos: handler returned nil response", 0) end + server_view:send(M.HelloReply_encode(resp)) + end, + }, + ["/hello.Greeter/Chat"] = { + kind = 'bidi', + handler = function(_, server_view, ctx) + local handler = impl.Chat + if handler == nil then error("Greeter.Chat: handler missing", 0) end + local wrapped = pb.grpc.wrap_server_view(server_view, M.HelloRequest_decode, M.HelloReply_encode) + handler(wrapped, ctx) + end, + }, + }, + } +end + +return M diff --git a/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua new file mode 100644 index 0000000000000000000000000000000000000000..5551d6dc8d6ed1f2c99e81fcb712a874a43fb29e --- /dev/null +++ b/examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua @@ -0,0 +1,257 @@ +-- Code generated by protoc-gen-tarantool. DO NOT EDIT. +-- source: test_messages_proto3.proto +-- syntax: proto3 +-- package: protobuf_test_messages.proto3 + +local pb = require("pb") +local wire = pb.wire + +local M = {} + +-- Enum: protobuf_test_messages.proto3.ForeignEnum +M.ForeignEnum_descriptor = pb.enum("protobuf_test_messages.proto3.ForeignEnum", { + FOREIGN_FOO = 0, + FOREIGN_BAR = 1, + FOREIGN_BAZ = 2, +}) +M.ForeignEnum = M.ForeignEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum +M.TestAllTypesProto3_NestedEnum_descriptor = pb.enum("protobuf_test_messages.proto3.TestAllTypesProto3.NestedEnum", { + FOO = 0, + BAR = 1, + BAZ = 2, + NEG = -1, +}) +M.TestAllTypesProto3_NestedEnum = M.TestAllTypesProto3_NestedEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum +M.TestAllTypesProto3_AliasedEnum_descriptor = pb.enum("protobuf_test_messages.proto3.TestAllTypesProto3.AliasedEnum", { + ALIAS_FOO = 0, + ALIAS_BAR = 1, + ALIAS_BAZ = 2, + MOO = 2, + moo = 2, + bAz = 2, +}) +M.TestAllTypesProto3_AliasedEnum = M.TestAllTypesProto3_AliasedEnum_descriptor.by_name + +-- Enum: protobuf_test_messages.proto3.EnumOnlyProto3.Bool +M.EnumOnlyProto3_Bool_descriptor = pb.enum("protobuf_test_messages.proto3.EnumOnlyProto3.Bool", { + kFalse = 0, + kTrue = 1, +}) +M.EnumOnlyProto3_Bool = M.EnumOnlyProto3_Bool_descriptor.by_name + +-- Pre-declare message descriptors so cross-references resolve. +M.TestAllTypesProto3_descriptor = {name = "protobuf_test_messages.proto3.TestAllTypesProto3"} +M.TestAllTypesProto3_NestedMessage_descriptor = {name = "protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage"} +M.ForeignMessage_descriptor = {name = "protobuf_test_messages.proto3.ForeignMessage"} +M.NullHypothesisProto3_descriptor = {name = "protobuf_test_messages.proto3.NullHypothesisProto3"} +M.EnumOnlyProto3_descriptor = {name = "protobuf_test_messages.proto3.EnumOnlyProto3"} + +-- Message: protobuf_test_messages.proto3.TestAllTypesProto3 +M.TestAllTypesProto3_descriptor.fields = { + {name="optional_int32", id=1, kind='scalar', proto_type="int32"}, + {name="optional_int64", id=2, kind='scalar', proto_type="int64"}, + {name="optional_uint32", id=3, kind='scalar', proto_type="uint32"}, + {name="optional_uint64", id=4, kind='scalar', proto_type="uint64"}, + {name="optional_sint32", id=5, kind='scalar', proto_type="sint32"}, + {name="optional_sint64", id=6, kind='scalar', proto_type="sint64"}, + {name="optional_fixed32", id=7, kind='scalar', proto_type="fixed32"}, + {name="optional_fixed64", id=8, kind='scalar', proto_type="fixed64"}, + {name="optional_sfixed32", id=9, kind='scalar', proto_type="sfixed32"}, + {name="optional_sfixed64", id=10, kind='scalar', proto_type="sfixed64"}, + {name="optional_float", id=11, kind='scalar', proto_type="float"}, + {name="optional_double", id=12, kind='scalar', proto_type="double"}, + {name="optional_bool", id=13, kind='scalar', proto_type="bool"}, + {name="optional_string", id=14, kind='scalar', proto_type="string"}, + {name="optional_bytes", id=15, kind='scalar', proto_type="bytes"}, + {name="optional_nested_message", id=18, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor}, + {name="optional_foreign_message", id=19, kind='message', message=M.ForeignMessage_descriptor}, + {name="optional_nested_enum", id=21, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor}, + {name="optional_foreign_enum", id=22, kind='enum', enum=M.ForeignEnum_descriptor}, + {name="optional_aliased_enum", id=23, kind='enum', enum=M.TestAllTypesProto3_AliasedEnum_descriptor}, + {name="optional_string_piece", id=24, kind='scalar', proto_type="string"}, + {name="optional_cord", id=25, kind='scalar', proto_type="string"}, + {name="recursive_message", id=27, kind='message', message=M.TestAllTypesProto3_descriptor}, + {name="repeated_int32", id=31, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="repeated_int64", id=32, kind='scalar', proto_type="int64", repeated=true, packed=true}, + {name="repeated_uint32", id=33, kind='scalar', proto_type="uint32", repeated=true, packed=true}, + {name="repeated_uint64", id=34, kind='scalar', proto_type="uint64", repeated=true, packed=true}, + {name="repeated_sint32", id=35, kind='scalar', proto_type="sint32", repeated=true, packed=true}, + {name="repeated_sint64", id=36, kind='scalar', proto_type="sint64", repeated=true, packed=true}, + {name="repeated_fixed32", id=37, kind='scalar', proto_type="fixed32", repeated=true, packed=true}, + {name="repeated_fixed64", id=38, kind='scalar', proto_type="fixed64", repeated=true, packed=true}, + {name="repeated_sfixed32", id=39, kind='scalar', proto_type="sfixed32", repeated=true, packed=true}, + {name="repeated_sfixed64", id=40, kind='scalar', proto_type="sfixed64", repeated=true, packed=true}, + {name="repeated_float", id=41, kind='scalar', proto_type="float", repeated=true, packed=true}, + {name="repeated_double", id=42, kind='scalar', proto_type="double", repeated=true, packed=true}, + {name="repeated_bool", id=43, kind='scalar', proto_type="bool", repeated=true, packed=true}, + {name="repeated_string", id=44, kind='scalar', proto_type="string", repeated=true}, + {name="repeated_bytes", id=45, kind='scalar', proto_type="bytes", repeated=true}, + {name="repeated_nested_message", id=48, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor, repeated=true}, + {name="repeated_foreign_message", id=49, kind='message', message=M.ForeignMessage_descriptor, repeated=true}, + {name="repeated_nested_enum", id=51, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true}, + {name="repeated_foreign_enum", id=52, kind='enum', enum=M.ForeignEnum_descriptor, repeated=true, packed=true}, + {name="repeated_string_piece", id=54, kind='scalar', proto_type="string", repeated=true}, + {name="repeated_cord", id=55, kind='scalar', proto_type="string", repeated=true}, + {name="packed_int32", id=75, kind='scalar', proto_type="int32", repeated=true, packed=true}, + {name="packed_int64", id=76, kind='scalar', proto_type="int64", repeated=true, packed=true}, + {name="packed_uint32", id=77, kind='scalar', proto_type="uint32", repeated=true, packed=true}, + {name="packed_uint64", id=78, kind='scalar', proto_type="uint64", repeated=true, packed=true}, + {name="packed_sint32", id=79, kind='scalar', proto_type="sint32", repeated=true, packed=true}, + {name="packed_sint64", id=80, kind='scalar', proto_type="sint64", repeated=true, packed=true}, + {name="packed_fixed32", id=81, kind='scalar', proto_type="fixed32", repeated=true, packed=true}, + {name="packed_fixed64", id=82, kind='scalar', proto_type="fixed64", repeated=true, packed=true}, + {name="packed_sfixed32", id=83, kind='scalar', proto_type="sfixed32", repeated=true, packed=true}, + {name="packed_sfixed64", id=84, kind='scalar', proto_type="sfixed64", repeated=true, packed=true}, + {name="packed_float", id=85, kind='scalar', proto_type="float", repeated=true, packed=true}, + {name="packed_double", id=86, kind='scalar', proto_type="double", repeated=true, packed=true}, + {name="packed_bool", id=87, kind='scalar', proto_type="bool", repeated=true, packed=true}, + {name="packed_nested_enum", id=88, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true}, + {name="unpacked_int32", id=89, kind='scalar', proto_type="int32", repeated=true, packed=false}, + {name="unpacked_int64", id=90, kind='scalar', proto_type="int64", repeated=true, packed=false}, + {name="unpacked_uint32", id=91, kind='scalar', proto_type="uint32", repeated=true, packed=false}, + {name="unpacked_uint64", id=92, kind='scalar', proto_type="uint64", repeated=true, packed=false}, + {name="unpacked_sint32", id=93, kind='scalar', proto_type="sint32", repeated=true, packed=false}, + {name="unpacked_sint64", id=94, kind='scalar', proto_type="sint64", repeated=true, packed=false}, + {name="unpacked_fixed32", id=95, kind='scalar', proto_type="fixed32", repeated=true, packed=false}, + {name="unpacked_fixed64", id=96, kind='scalar', proto_type="fixed64", repeated=true, packed=false}, + {name="unpacked_sfixed32", id=97, kind='scalar', proto_type="sfixed32", repeated=true, packed=false}, + {name="unpacked_sfixed64", id=98, kind='scalar', proto_type="sfixed64", repeated=true, packed=false}, + {name="unpacked_float", id=99, kind='scalar', proto_type="float", repeated=true, packed=false}, + {name="unpacked_double", id=100, kind='scalar', proto_type="double", repeated=true, packed=false}, + {name="unpacked_bool", id=101, kind='scalar', proto_type="bool", repeated=true, packed=false}, + {name="unpacked_nested_enum", id=102, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=false}, + {name="map_int32_int32", id=56, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="int32"}}, + {name="map_int64_int64", id=57, kind='map', key={kind='scalar', proto_type="int64"}, value={kind='scalar', proto_type="int64"}}, + {name="map_uint32_uint32", id=58, kind='map', key={kind='scalar', proto_type="uint32"}, value={kind='scalar', proto_type="uint32"}}, + {name="map_uint64_uint64", id=59, kind='map', key={kind='scalar', proto_type="uint64"}, value={kind='scalar', proto_type="uint64"}}, + {name="map_sint32_sint32", id=60, kind='map', key={kind='scalar', proto_type="sint32"}, value={kind='scalar', proto_type="sint32"}}, + {name="map_sint64_sint64", id=61, kind='map', key={kind='scalar', proto_type="sint64"}, value={kind='scalar', proto_type="sint64"}}, + {name="map_fixed32_fixed32", id=62, kind='map', key={kind='scalar', proto_type="fixed32"}, value={kind='scalar', proto_type="fixed32"}}, + {name="map_fixed64_fixed64", id=63, kind='map', key={kind='scalar', proto_type="fixed64"}, value={kind='scalar', proto_type="fixed64"}}, + {name="map_sfixed32_sfixed32", id=64, kind='map', key={kind='scalar', proto_type="sfixed32"}, value={kind='scalar', proto_type="sfixed32"}}, + {name="map_sfixed64_sfixed64", id=65, kind='map', key={kind='scalar', proto_type="sfixed64"}, value={kind='scalar', proto_type="sfixed64"}}, + {name="map_int32_float", id=66, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="float"}}, + {name="map_int32_double", id=67, kind='map', key={kind='scalar', proto_type="int32"}, value={kind='scalar', proto_type="double"}}, + {name="map_bool_bool", id=68, kind='map', key={kind='scalar', proto_type="bool"}, value={kind='scalar', proto_type="bool"}}, + {name="map_string_string", id=69, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="string"}}, + {name="map_string_bytes", id=70, kind='map', key={kind='scalar', proto_type="string"}, value={kind='scalar', proto_type="bytes"}}, + {name="map_string_nested_message", id=71, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor}}, + {name="map_string_foreign_message", id=72, kind='map', key={kind='scalar', proto_type="string"}, value={kind='message', message=M.ForeignMessage_descriptor}}, + {name="map_string_nested_enum", id=73, kind='map', key={kind='scalar', proto_type="string"}, value={kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor}}, + {name="map_string_foreign_enum", id=74, kind='map', key={kind='scalar', proto_type="string"}, value={kind='enum', enum=M.ForeignEnum_descriptor}}, + {name="oneof_uint32", id=111, kind='scalar', proto_type="uint32", oneof="oneof_field"}, + {name="oneof_nested_message", id=112, kind='message', message=M.TestAllTypesProto3_NestedMessage_descriptor, oneof="oneof_field"}, + {name="oneof_string", id=113, kind='scalar', proto_type="string", oneof="oneof_field"}, + {name="oneof_bytes", id=114, kind='scalar', proto_type="bytes", oneof="oneof_field"}, + {name="oneof_bool", id=115, kind='scalar', proto_type="bool", oneof="oneof_field"}, + {name="oneof_uint64", id=116, kind='scalar', proto_type="uint64", oneof="oneof_field"}, + {name="oneof_float", id=117, kind='scalar', proto_type="float", oneof="oneof_field"}, + {name="oneof_double", id=118, kind='scalar', proto_type="double", oneof="oneof_field"}, + {name="oneof_enum", id=119, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, oneof="oneof_field"}, + {name="oneof_null_value", id=120, kind='enum', enum=pb.wkt.NullValue_descriptor, oneof="oneof_field"}, + {name="optional_bool_wrapper", id=201, kind='message', message=pb.wkt.BoolValue_descriptor}, + {name="optional_int32_wrapper", id=202, kind='message', message=pb.wkt.Int32Value_descriptor}, + {name="optional_int64_wrapper", id=203, kind='message', message=pb.wkt.Int64Value_descriptor}, + {name="optional_uint32_wrapper", id=204, kind='message', message=pb.wkt.UInt32Value_descriptor}, + {name="optional_uint64_wrapper", id=205, kind='message', message=pb.wkt.UInt64Value_descriptor}, + {name="optional_float_wrapper", id=206, kind='message', message=pb.wkt.FloatValue_descriptor}, + {name="optional_double_wrapper", id=207, kind='message', message=pb.wkt.DoubleValue_descriptor}, + {name="optional_string_wrapper", id=208, kind='message', message=pb.wkt.StringValue_descriptor}, + {name="optional_bytes_wrapper", id=209, kind='message', message=pb.wkt.BytesValue_descriptor}, + {name="repeated_bool_wrapper", id=211, kind='message', message=pb.wkt.BoolValue_descriptor, repeated=true}, + {name="repeated_int32_wrapper", id=212, kind='message', message=pb.wkt.Int32Value_descriptor, repeated=true}, + {name="repeated_int64_wrapper", id=213, kind='message', message=pb.wkt.Int64Value_descriptor, repeated=true}, + {name="repeated_uint32_wrapper", id=214, kind='message', message=pb.wkt.UInt32Value_descriptor, repeated=true}, + {name="repeated_uint64_wrapper", id=215, kind='message', message=pb.wkt.UInt64Value_descriptor, repeated=true}, + {name="repeated_float_wrapper", id=216, kind='message', message=pb.wkt.FloatValue_descriptor, repeated=true}, + {name="repeated_double_wrapper", id=217, kind='message', message=pb.wkt.DoubleValue_descriptor, repeated=true}, + {name="repeated_string_wrapper", id=218, kind='message', message=pb.wkt.StringValue_descriptor, repeated=true}, + {name="repeated_bytes_wrapper", id=219, kind='message', message=pb.wkt.BytesValue_descriptor, repeated=true}, + {name="optional_duration", id=301, kind='message', message=pb.wkt.Duration_descriptor}, + {name="optional_timestamp", id=302, kind='message', message=pb.wkt.Timestamp_descriptor}, + {name="optional_field_mask", id=303, kind='message', message=pb.wkt.FieldMask_descriptor}, + {name="optional_struct", id=304, kind='message', message=pb.wkt.Struct_descriptor}, + {name="optional_any", id=305, kind='message', message=pb.wkt.Any_descriptor}, + {name="optional_value", id=306, kind='message', message=pb.wkt.Value_descriptor}, + {name="optional_null_value", id=307, kind='enum', enum=pb.wkt.NullValue_descriptor}, + {name="optional_empty", id=308, kind='message', message=pb.wkt.Empty_descriptor}, + {name="repeated_duration", id=311, kind='message', message=pb.wkt.Duration_descriptor, repeated=true}, + {name="repeated_timestamp", id=312, kind='message', message=pb.wkt.Timestamp_descriptor, repeated=true}, + {name="repeated_fieldmask", id=313, kind='message', message=pb.wkt.FieldMask_descriptor, repeated=true}, + {name="repeated_struct", id=324, kind='message', message=pb.wkt.Struct_descriptor, repeated=true}, + {name="repeated_any", id=315, kind='message', message=pb.wkt.Any_descriptor, repeated=true}, + {name="repeated_value", id=316, kind='message', message=pb.wkt.Value_descriptor, repeated=true}, + {name="repeated_list_value", id=317, kind='message', message=pb.wkt.ListValue_descriptor, repeated=true}, + {name="repeated_empty", id=318, kind='message', message=pb.wkt.Empty_descriptor, repeated=true}, + {name="fieldname1", id=401, kind='scalar', proto_type="int32"}, + {name="field_name2", id=402, kind='scalar', proto_type="int32"}, + {name="_field_name3", id=403, kind='scalar', proto_type="int32"}, + {name="field__name4_", id=404, kind='scalar', proto_type="int32"}, + {name="field0name5", id=405, kind='scalar', proto_type="int32"}, + {name="field_0_name6", id=406, kind='scalar', proto_type="int32"}, + {name="fieldName7", id=407, kind='scalar', proto_type="int32"}, + {name="FieldName8", id=408, kind='scalar', proto_type="int32"}, + {name="field_Name9", id=409, kind='scalar', proto_type="int32"}, + {name="Field_Name10", id=410, kind='scalar', proto_type="int32"}, + {name="FIELD_NAME11", id=411, kind='scalar', proto_type="int32"}, + {name="FIELD_name12", id=412, kind='scalar', proto_type="int32"}, + {name="__field_name13", id=413, kind='scalar', proto_type="int32"}, + {name="__Field_name14", id=414, kind='scalar', proto_type="int32"}, + {name="field__name15", id=415, kind='scalar', proto_type="int32"}, + {name="field__Name16", id=416, kind='scalar', proto_type="int32"}, + {name="field_name17__", id=417, kind='scalar', proto_type="int32"}, + {name="Field_name18__", id=418, kind='scalar', proto_type="int32"}, +} +M.TestAllTypesProto3_descriptor.oneofs = { + oneof_field = {"oneof_uint32", "oneof_nested_message", "oneof_string", "oneof_bytes", "oneof_bool", "oneof_uint64", "oneof_float", "oneof_double", "oneof_enum", "oneof_null_value"}, +} +pb.finalize_message(M.TestAllTypesProto3_descriptor) + +-- Message: protobuf_test_messages.proto3.TestAllTypesProto3.NestedMessage +M.TestAllTypesProto3_NestedMessage_descriptor.fields = { + {name="a", id=1, kind='scalar', proto_type="int32"}, + {name="corecursive", id=2, kind='message', message=M.TestAllTypesProto3_descriptor}, +} +pb.finalize_message(M.TestAllTypesProto3_NestedMessage_descriptor) + +-- Message: protobuf_test_messages.proto3.ForeignMessage +M.ForeignMessage_descriptor.fields = { + {name="c", id=1, kind='scalar', proto_type="int32"}, +} +pb.finalize_message(M.ForeignMessage_descriptor) + +-- Message: protobuf_test_messages.proto3.NullHypothesisProto3 +M.NullHypothesisProto3_descriptor.fields = { +} +pb.finalize_message(M.NullHypothesisProto3_descriptor) + +-- Message: protobuf_test_messages.proto3.EnumOnlyProto3 +M.EnumOnlyProto3_descriptor.fields = { +} +pb.finalize_message(M.EnumOnlyProto3_descriptor) + +function M.TestAllTypesProto3_new(t) return t or {} end +function M.TestAllTypesProto3_encode(t) return pb.encode(M.TestAllTypesProto3_descriptor, t) end +function M.TestAllTypesProto3_decode(b) return pb.decode(M.TestAllTypesProto3_descriptor, b) end + +function M.TestAllTypesProto3_NestedMessage_new(t) return t or {} end +function M.TestAllTypesProto3_NestedMessage_encode(t) return pb.encode(M.TestAllTypesProto3_NestedMessage_descriptor, t) end +function M.TestAllTypesProto3_NestedMessage_decode(b) return pb.decode(M.TestAllTypesProto3_NestedMessage_descriptor, b) end + +function M.ForeignMessage_new(t) return t or {} end +function M.ForeignMessage_encode(t) return pb.encode(M.ForeignMessage_descriptor, t) end +function M.ForeignMessage_decode(b) return pb.decode(M.ForeignMessage_descriptor, b) end + +function M.NullHypothesisProto3_new(t) return t or {} end +function M.NullHypothesisProto3_encode(t) return pb.encode(M.NullHypothesisProto3_descriptor, t) end +function M.NullHypothesisProto3_decode(b) return pb.decode(M.NullHypothesisProto3_descriptor, b) end + +function M.EnumOnlyProto3_new(t) return t or {} end +function M.EnumOnlyProto3_encode(t) return pb.encode(M.EnumOnlyProto3_descriptor, t) end +function M.EnumOnlyProto3_decode(b) return pb.decode(M.EnumOnlyProto3_descriptor, b) end + +return M diff --git a/examples/proto/hello.proto b/examples/proto/hello.proto new file mode 100644 index 0000000000000000000000000000000000000000..db37429bd616f5dcf6ce8bd479c6278f11c8446f --- /dev/null +++ b/examples/proto/hello.proto @@ -0,0 +1,91 @@ +syntax = "proto3"; + +package hello; + +import "google/protobuf/timestamp.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/wrappers.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/any.proto"; +import "google/protobuf/field_mask.proto"; + +// option (tarantool.lua_package) = "hello.foo"; // optional override + +enum Status { + UNKNOWN = 0; + OK = 1; + ERROR = 2; +} + +// Demo message for oneof handling. +message Result { + int32 id = 1; + oneof outcome { + string text = 2; + int32 code = 3; + Address details = 4; + } +} + +// gRPC service demo. Covers unary + all three streaming flavors so the +// loopback transport exercises every codegen branch. +message HelloRequest { + string name = 1; +} +message HelloReply { + string greeting = 1; +} +service Greeter { + rpc SayHello(HelloRequest) returns (HelloReply); + rpc Echo(HelloRequest) returns (HelloRequest); + // Server-streaming: one request, server pushes N replies. + rpc StreamHellos(HelloRequest) returns (stream HelloReply); + // Client-streaming: client pushes N requests, server returns one reply. + rpc CollectHellos(stream HelloRequest) returns (HelloReply); + // Bidirectional: both sides push and pull independently. + rpc Chat(stream HelloRequest) returns (stream HelloReply); +} + +// Demo message exercising well-known types (M3). +message Event { + string title = 1; + google.protobuf.Timestamp created_at = 2; + google.protobuf.Duration duration = 3; + google.protobuf.Empty ack = 4; + google.protobuf.Int32Value retry_count = 5; + google.protobuf.StringValue note = 6; + google.protobuf.BoolValue is_admin = 7; + google.protobuf.Struct payload = 8; + google.protobuf.Value attribute = 9; + google.protobuf.ListValue tags = 10; + google.protobuf.Any extension = 11; + google.protobuf.FieldMask update_mask = 12; +} + +message Address { + string street = 1; + string city = 2; + int32 zip = 3; + // Explicit-optional: presence is meaningful (distinct from default). + optional string apartment = 4; +} + +message Person { + string name = 1; + int32 age = 2; + repeated string emails = 3; + Status status = 4; + Address address = 5; + repeated Person friends = 6; // self-reference + repeated int32 lucky_numbers = 7; // packed by default + bytes avatar = 8; + fixed64 user_id = 9; + sint32 balance = 10; // zigzag + double weight_kg = 11; + + // Map fields (M2) + map ages_by_nickname = 13; // scalar value + map nickname_by_age = 14; // scalar key + value + map addresses_by_label = 15; // message value +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000000000000000000000000000000000000..3efe4598fe1dbb95ee5ab14d9bbeb4560a1663a1 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module sourcecraft.dev/bigbes/tarantool-protobuf + +go 1.23 + +require google.golang.org/protobuf v1.36.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000000000000000000000000000000000000..c3533990285a1ebef4e012bd76753e1bfdbc7c24 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= diff --git a/options/tarantool/tarantool.proto b/options/tarantool/tarantool.proto new file mode 100644 index 0000000000000000000000000000000000000000..87aea13718149951468698b0c69ae366dc31e239 --- /dev/null +++ b/options/tarantool/tarantool.proto @@ -0,0 +1,21 @@ +// Custom protobuf options for protoc-gen-tarantool. +// +// To use, place this file on your protoc include path and add to your .proto: +// +// import "tarantool/tarantool.proto"; +// option (tarantool.lua_package) = "myapp.proto.foo"; +// +syntax = "proto3"; + +package tarantool; + +import "google/protobuf/descriptor.proto"; + +option go_package = "sourcecraft.dev/bigbes/tarantool-protobuf/options/tarantool;tarantoolpb"; + +extend google.protobuf.FileOptions { + // Overrides the Lua require path for the generated module. + // If unset, the path is derived from the proto package + filename: + // package foo.bar; file baz.proto -> "foo.bar.baz_pb" + string lua_package = 60001; +} diff --git a/runtime/pb/codec.lua b/runtime/pb/codec.lua new file mode 100644 index 0000000000000000000000000000000000000000..10a198bf1294fa6daddbc8f17fdab5398487f385 --- /dev/null +++ b/runtime/pb/codec.lua @@ -0,0 +1,424 @@ +-- Descriptor-driven protobuf encoder/decoder (proto3). +-- Operates on the descriptor tables emitted by protoc-gen-tarantool. +-- +-- Descriptor shape: +-- { +-- name = 'pkg.Foo', +-- fields = { ... }, -- ordered, used for encode +-- field_by_id = { [id] = ... }, -- used for decode +-- } +-- +-- Field shape: +-- { name='x', id=1, kind=<'scalar'|'message'|'enum'|'map'>, +-- proto_type=, -- when kind='scalar' +-- message=, -- when kind='message' +-- enum=, -- when kind='enum' +-- key=, -- when kind='map' (synthetic id=1) +-- value=, -- when kind='map' (synthetic id=2) +-- repeated=true|nil, +-- packed=true|nil, -- only meaningful when repeated and scalar +-- oneof=|nil, -- set on oneof branches +-- optional=true|nil } -- proto3 explicit optional (presence) +-- +-- Enum descriptor shape (no encode/decode logic; used for value lookup): +-- { name='pkg.Color', by_name={RED=0,...}, by_value={[0]='RED',...} } +local ffi = require('ffi') +local wire = require('pb.wire') + +local M = {} + +local UINT64 = ffi.typeof('uint64_t') +local UINT64_ZERO = UINT64(0) + +-- Reuse the single source of truth defined in pb.wire. +local scalar = wire.TYPE_INFO +M.scalar = scalar + +-- --------------------------------------------------------------------------- +-- proto3 default-value detection (for elision on encode) +-- --------------------------------------------------------------------------- + +local function is_default_scalar(proto_type, v) + if proto_type == 'string' or proto_type == 'bytes' then + return v == '' + elseif proto_type == 'bool' then + return v == false + elseif proto_type == 'float' or proto_type == 'double' then + return v == 0 + elseif proto_type == 'int64' or proto_type == 'uint64' + or proto_type == 'sint64' or proto_type == 'fixed64' + or proto_type == 'sfixed64' then + if type(v) == 'cdata' then return v == UINT64_ZERO or v == ffi.cast('int64_t', 0) end + return v == 0 + end + -- All remaining numeric scalars compare against Lua 0. + return v == 0 +end + +-- --------------------------------------------------------------------------- +-- Encode +-- --------------------------------------------------------------------------- + +-- Forward declaration so encode_field can recurse via encode_message. +local encode_message + +local function encode_enum_value(enum_desc, v) + if type(v) == 'number' then return v end + if type(v) == 'string' then + local n = enum_desc.by_name[v] + if n == nil then + error(("unknown enum value '%s' for %s"):format(v, enum_desc.name), 0) + end + return n + end + error("enum value must be a number or string", 0) +end + +-- encode_msg dispatches to a descriptor's custom encode function (used by +-- WKT) when present; otherwise walks fields generically. +local function encode_msg(desc, value) + if desc.encode then return desc.encode(value) end + return encode_message(desc, value) +end + +-- encode_one returns wire bytes for a single value (no tag), based on the +-- field's kind/proto_type. Used for map keys/values and as a building block. +local function encode_one(field, v) + local kind = field.kind + if kind == 'scalar' then + return scalar[field.proto_type].encode(v) + elseif kind == 'enum' then + return wire.encode_varint(encode_enum_value(field.enum, v)) + elseif kind == 'message' then + return wire.encode_len(encode_msg(field.message, v)) + end + error("encode_one: unknown kind " .. tostring(kind), 0) +end + +-- wire_type_for returns the wire type for a field's value (singular). +local function wire_type_for(field) + local kind = field.kind + if kind == 'scalar' then return scalar[field.proto_type].wire end + if kind == 'enum' then return wire.WIRE_VARINT end + if kind == 'message' then return wire.WIRE_LEN end + error("wire_type_for: unknown kind " .. tostring(kind), 0) +end + +-- encode_field writes one field's bytes to `out`. When `force` is true, +-- proto3 default-value elision is disabled (used for oneof branches where +-- the user's intent to set a default value is meaningful). +local function encode_field(field, value, out, force) + local id = field.id + local kind = field.kind + + if kind == 'map' then + if value == nil or next(value) == nil then return end + local tag = wire.encode_tag(id, wire.WIRE_LEN) + local key_field, value_field = field.key, field.value + local key_tag = wire.encode_tag(1, wire_type_for(key_field)) + local val_tag = wire.encode_tag(2, wire_type_for(value_field)) + for k, v in pairs(value) do + local entry = {} + -- Defaults round-trip through proto3 elision; emit only non-defaults. + if not is_default_scalar(key_field.proto_type or '', k) then + entry[#entry + 1] = key_tag + entry[#entry + 1] = encode_one(key_field, k) + end + local v_kind = value_field.kind + local skip_v = false + if v_kind == 'scalar' then + skip_v = is_default_scalar(value_field.proto_type, v) + elseif v_kind == 'enum' then + skip_v = (encode_enum_value(value_field.enum, v) == 0) + end + if not skip_v then + entry[#entry + 1] = val_tag + entry[#entry + 1] = encode_one(value_field, v) + end + out[#out + 1] = tag + out[#out + 1] = wire.encode_len(table.concat(entry)) + end + return + end + + if field.repeated then + if value == nil or #value == 0 then return end + + if kind == 'scalar' then + local h = scalar[field.proto_type] + if not h then error("unknown scalar " .. tostring(field.proto_type), 0) end + + if field.packed and h.packable then + local parts = {} + for i = 1, #value do parts[i] = h.encode(value[i]) end + local payload = table.concat(parts) + out[#out + 1] = wire.encode_tag(id, wire.WIRE_LEN) + out[#out + 1] = wire.encode_len(payload) + else + local tag = wire.encode_tag(id, h.wire) + for i = 1, #value do + out[#out + 1] = tag + out[#out + 1] = h.encode(value[i]) + end + end + elseif kind == 'enum' then + -- Repeated enums are packed by default in proto3. + if field.packed ~= false then + local parts = {} + for i = 1, #value do + parts[i] = wire.encode_varint(encode_enum_value(field.enum, value[i])) + end + out[#out + 1] = wire.encode_tag(id, wire.WIRE_LEN) + out[#out + 1] = wire.encode_len(table.concat(parts)) + else + local tag = wire.encode_tag(id, wire.WIRE_VARINT) + for i = 1, #value do + out[#out + 1] = tag + out[#out + 1] = wire.encode_varint(encode_enum_value(field.enum, value[i])) + end + end + elseif kind == 'message' then + local tag = wire.encode_tag(id, wire.WIRE_LEN) + for i = 1, #value do + out[#out + 1] = tag + out[#out + 1] = wire.encode_len(encode_msg(field.message, value[i])) + end + else + error("unknown field kind " .. tostring(kind), 0) + end + return + end + + -- Singular field + if value == nil then return end + + if kind == 'scalar' then + if not force and is_default_scalar(field.proto_type, value) then return end + local h = scalar[field.proto_type] + if not h then error("unknown scalar " .. tostring(field.proto_type), 0) end + out[#out + 1] = wire.encode_tag(id, h.wire) + out[#out + 1] = h.encode(value) + elseif kind == 'enum' then + local n = encode_enum_value(field.enum, value) + if not force and n == 0 then return end -- proto3 default + out[#out + 1] = wire.encode_tag(id, wire.WIRE_VARINT) + out[#out + 1] = wire.encode_varint(n) + elseif kind == 'message' then + out[#out + 1] = wire.encode_tag(id, wire.WIRE_LEN) + out[#out + 1] = wire.encode_len(encode_msg(field.message, value)) + else + error("unknown field kind " .. tostring(kind), 0) + end +end + +encode_message = function(desc, data) + if type(data) ~= 'table' then + error(("expected table for message %s, got %s"):format(desc.name, type(data)), 0) + end + local out = {} + local fields = desc.fields + + -- For each oneof, pick the active branch (last set in declaration order). + local active -- {[oneof_name] = field_name} or nil + if desc.oneofs then + active = {} + for oname, members in pairs(desc.oneofs) do + for _, fname in ipairs(members) do + if data[fname] ~= nil then active[oname] = fname end + end + end + end + + for i = 1, #fields do + local f = fields[i] + if f.oneof then + if active and active[f.oneof] == f.name then + encode_field(f, data[f.name], out, true) -- force: emit even defaults + end + else + -- Optional fields have presence: emit even defaults when set. + encode_field(f, data[f.name], out, f.optional) + end + end + -- Preserve unknown fields captured at decode time. + local uf = data._unknown_fields + if uf ~= nil and uf ~= '' then out[#out + 1] = uf end + return table.concat(out) +end +M.encode = encode_message + +-- --------------------------------------------------------------------------- +-- Decode +-- --------------------------------------------------------------------------- + +local decode_message + +-- decode_msg dispatches to a descriptor's custom decode (WKT) when present. +local function decode_msg(desc, buf) + if desc.decode then return desc.decode(buf) end + return decode_message(desc, buf) +end + +-- decode_one returns (value, new_pos) for a single value based on field kind. +local function decode_one(field, buf, pos) + local kind = field.kind + if kind == 'scalar' then + return scalar[field.proto_type].decode(buf, pos) + elseif kind == 'enum' then + local u, np = wire.decode_varint(buf, pos) + return tonumber(u), np + elseif kind == 'message' then + local payload, np = wire.decode_len(buf, pos) + return decode_msg(field.message, payload), np + end + error("decode_one: unknown kind " .. tostring(kind), 0) +end + +-- default_value returns the proto3 zero value for a (sub-)field descriptor. +local function default_value(field) + local kind = field.kind + if kind == 'scalar' then + local pt = field.proto_type + if pt == 'string' or pt == 'bytes' then return '' end + if pt == 'bool' then return false end + return 0 + elseif kind == 'enum' then + return 0 + elseif kind == 'message' then + return {} + end + error("default_value: unknown kind " .. tostring(kind), 0) +end + +local function decode_packed(field, payload) + local h = scalar[field.proto_type] + if not h then error("packed unknown scalar " .. tostring(field.proto_type), 0) end + local out, pos, len = {}, 1, #payload + local n = 0 + while pos <= len do + local v, np = h.decode(payload, pos) + n = n + 1 + out[n] = v + pos = np + end + return out +end + +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) + end + local result = {} + local pos, len = 1, #buf + local fbi = desc.field_by_id + local unknown -- list of raw tag+value byte slices, lazily allocated + + while pos <= len do + local tag_start = pos + local id, wt, npos = wire.decode_tag(buf, pos) + pos = npos + local f = fbi[id] + if f == nil then + -- Unknown field: capture verbatim for round-trip. + pos = wire.skip_field(buf, pos, wt) + if unknown == nil then unknown = {} end + unknown[#unknown + 1] = buf:sub(tag_start, pos - 1) + else + local kind = f.kind + if kind == 'map' then + local map_t = result[f.name] + if map_t == nil then map_t = {}; result[f.name] = map_t end + local payload, np = wire.decode_len(buf, pos) + pos = np + local key, val + local ep, elim = 1, #payload + while ep <= elim do + local eid, ewt + eid, ewt, ep = wire.decode_tag(payload, ep) + if eid == 1 then + key, ep = decode_one(f.key, payload, ep) + elseif eid == 2 then + val, ep = decode_one(f.value, payload, ep) + else + ep = wire.skip_field(payload, ep, ewt) + end + end + if key == nil then key = default_value(f.key) end + if val == nil then val = default_value(f.value) end + map_t[key] = val + elseif f.repeated then + local list = result[f.name] + if list == nil then list = {}; result[f.name] = list end + + if kind == 'scalar' then + local h = scalar[f.proto_type] + if h.packable and wt == wire.WIRE_LEN and h.wire ~= wire.WIRE_LEN then + -- Packed payload: decode all elements. + local payload, np = wire.decode_len(buf, pos) + pos = np + local items = decode_packed(f, payload) + local base = #list + for i = 1, #items do list[base + i] = items[i] end + else + local v, np = h.decode(buf, pos) + list[#list + 1] = v + pos = np + end + elseif kind == 'enum' then + if wt == wire.WIRE_LEN then + local payload, np = wire.decode_len(buf, pos) + pos = np + local p2, lim = 1, #payload + while p2 <= lim do + local u, np2 = wire.decode_varint(payload, p2) + p2 = np2 + list[#list + 1] = tonumber(u) + end + else + local u, np = wire.decode_varint(buf, pos) + list[#list + 1] = tonumber(u) + pos = np + end + elseif kind == 'message' then + local payload, np = wire.decode_len(buf, pos) + pos = np + list[#list + 1] = decode_msg(f.message, payload) + end + else + -- Singular + if kind == 'scalar' then + local h = scalar[f.proto_type] + local v, np = h.decode(buf, pos) + pos = np + result[f.name] = v + elseif kind == 'enum' then + local u, np = wire.decode_varint(buf, pos) + pos = np + result[f.name] = tonumber(u) + elseif kind == 'message' then + local payload, np = wire.decode_len(buf, pos) + pos = np + local decoded = decode_msg(f.message, payload) + -- Per spec: repeated singular message fields merge, + -- *unless* this is a oneof branch (exclusive). WKT + -- types use custom decode and aren't merged either. + local prev = result[f.name] + if prev == nil or f.oneof or f.message.decode then + result[f.name] = decoded + else + for k, v in pairs(decoded) do prev[k] = v end + end + end + -- Oneof: clear sibling branches. + if f.oneof_siblings then + for _, s in ipairs(f.oneof_siblings) do result[s] = nil end + end + end + end + end + if unknown ~= nil then result._unknown_fields = table.concat(unknown) end + return result +end +M.decode = decode_message + +return M diff --git a/runtime/pb/dynamic.lua b/runtime/pb/dynamic.lua new file mode 100644 index 0000000000000000000000000000000000000000..ff85f2caf7f10961cc97a67feea7f66f9c79b390 --- /dev/null +++ b/runtime/pb/dynamic.lua @@ -0,0 +1,235 @@ +-- Build a runtime module from a parsed .proto AST. +-- +-- Output shape mirrors what protoc-gen-tarantool emits in `mode=runtime`: +-- M._descriptor - field descriptors usable by pb.encode/decode +-- M._encode(t) - table -> wire bytes +-- M._decode(b) - wire bytes -> table +-- M. - alias for by_name table +-- M._descriptor - the enum descriptor +-- +-- WKT references (google.protobuf.*) are resolved against pb.wkt so dynamic +-- schemas can interop with the same Timestamp/Duration/wrapper sugar that +-- generated code uses. +local codec = require('pb.codec') +local wire = require('pb.wire') +local wkt = require('pb.wkt') + +local TYPE_INFO = wire.TYPE_INFO + +local M = {} + +local function short_name(full) + -- "pkg.sub.Name" -> "Name" (just the trailing segment) + return full:match('[^%.]+$') or full +end + +local function flat_name(full, pkg) + -- "pkg.Outer.Inner" with pkg="pkg" -> "Outer_Inner" + local s = full + if pkg ~= '' then s = s:gsub('^' .. pkg:gsub('%.', '%%.') .. '%.', '', 1) end + return s:gsub('%.', '_') +end + +local function make_enum_descriptor(name, values) + local desc = {name = name, by_name = {}, by_value = {}} + for k, v in pairs(values) do desc.by_name[k] = v; desc.by_value[v] = k end + return desc +end + +-- Pre-build a name->descriptor lookup over the entire AST. Resolution rules: +-- * Try the unqualified name as-is (innermost scope). +-- * Try fully-qualified within the current package. +-- * Try google.protobuf. via pb.wkt. +-- * Fall back to lookup by full name. +local function build_index(parsed, msg_descs, enum_descs) + local pkg_prefix = parsed.package ~= '' and (parsed.package .. '.') or '' + local index = {} -- {[any-form-of-name] = descriptor} + + for full, desc in pairs(msg_descs) do + index[full] = desc + index[short_name(full)] = desc + end + for full, desc in pairs(enum_descs) do + index[full] = desc + index[short_name(full)] = desc + end + return index, pkg_prefix +end + +-- Resolve a typename string from a field definition to (kind, descriptor-or-nil). +local function resolve_type(typename, index) + if TYPE_INFO[typename] then return 'scalar', typename end + -- Well-known types: accept "google.protobuf.Timestamp" or "Timestamp"-like. + local wkt_short = typename:match('^google%.protobuf%.(.+)$') or typename + local wkt_desc = wkt[wkt_short .. '_descriptor'] + if wkt_desc then return 'message', wkt_desc end + local d = index[typename] + if d then + if d.by_name then return 'enum', d end + return 'message', d + end + error("dynamic: cannot resolve type " .. typename, 0) +end + +-- Build a field descriptor for an ordinary (non-map) field. +local function build_field(field_ast, index) + local entry = {name = field_ast.name, id = field_ast.id} + local kind, ref = resolve_type(field_ast.type, index) + entry.kind = kind + if kind == 'scalar' then + entry.proto_type = ref + elseif kind == 'message' then + entry.message = ref + elseif kind == 'enum' then + entry.enum = ref + end + if field_ast.repeated then + entry.repeated = true + -- proto3 packing default: packed for primitives + enums, + -- never for string/bytes/message. + if kind ~= 'message' and field_ast.type ~= 'string' and field_ast.type ~= 'bytes' then + entry.packed = (field_ast.packed ~= false) + end + end + if field_ast.oneof then entry.oneof = field_ast.oneof end + if field_ast.optional then entry.optional = true end + return entry +end + +local function build_map_field(field_ast, index) + local key_kind, key_ref = resolve_type(field_ast.key_type, index) + local val_kind, val_ref = resolve_type(field_ast.value_type, index) + local entry = {name = field_ast.name, id = field_ast.id, kind = 'map'} + local key_desc = {kind = key_kind} + if key_kind == 'scalar' then key_desc.proto_type = key_ref end + local val_desc = {kind = val_kind} + if val_kind == 'scalar' then val_desc.proto_type = val_ref + elseif val_kind == 'message' then val_desc.message = val_ref + elseif val_kind == 'enum' then val_desc.enum = val_ref end + entry.key = key_desc + entry.value = val_desc + return entry +end + +-- --------------------------------------------------------------------------- +-- Walk a message AST recursively, returning flat (full_name, ast) pairs for +-- the message itself and every nested message + enum. +-- --------------------------------------------------------------------------- +local function flatten(parsed) + local msgs = {} -- declaration-ordered: {full_name=, ast=} + local enums = {} + local pkg = parsed.package + local function full_of(name, scope) + if scope ~= '' then return scope .. '.' .. name end + return name + end + local function walk_msg(ast, scope) + local full = full_of(ast.name, scope) + msgs[#msgs + 1] = {full_name = full, ast = ast} + for _, e in ipairs(ast.nested_enums) do + enums[#enums + 1] = {full_name = full_of(e.name, full), ast = e} + end + for _, n in ipairs(ast.nested_messages) do + walk_msg(n, full) + end + end + for _, e in ipairs(parsed.enums) do + enums[#enums + 1] = {full_name = full_of(e.name, pkg), ast = e} + end + for _, m in ipairs(parsed.messages) do walk_msg(m, pkg) end + return msgs, enums +end + +-- --------------------------------------------------------------------------- +-- M.build(parsed) -> module table +-- --------------------------------------------------------------------------- +function M.build(parsed) + local out = {} + local msgs, enums = flatten(parsed) + local pkg = parsed.package + + -- 1) Build enum descriptors. + local enum_descs = {} + for _, e in ipairs(enums) do + local desc = make_enum_descriptor(e.full_name, e.ast.values) + enum_descs[e.full_name] = desc + local flat = flat_name(e.full_name, pkg) + out[flat .. '_descriptor'] = desc + out[flat] = desc.by_name + end + + -- 2) Pre-declare message descriptors so cross-references resolve. + local msg_descs = {} + for _, m in ipairs(msgs) do + local desc = {name = m.full_name} + msg_descs[m.full_name] = desc + out[flat_name(m.full_name, pkg) .. '_descriptor'] = desc + end + + local index = build_index(parsed, msg_descs, enum_descs) + + -- 3) Fill in fields[] and oneofs for each message; finalize. + for _, m in ipairs(msgs) do + local desc = msg_descs[m.full_name] + desc.fields = {} + for _, f in ipairs(m.ast.fields) do + if f.kind == 'map' then + desc.fields[#desc.fields + 1] = build_map_field(f, index) + else + desc.fields[#desc.fields + 1] = build_field(f, index) + end + end + if #m.ast.oneofs > 0 then + desc.oneofs = {} + for _, oo in ipairs(m.ast.oneofs) do + desc.oneofs[oo.name] = oo.fields + end + end + codec.finalize_message = codec.finalize_message -- (no-op, just for clarity) + end + + -- We use the public finalize from init.lua, but it lives in the parent + -- module — replicate the work here to avoid a circular require. + for _, m in ipairs(msgs) do + local desc = msg_descs[m.full_name] + local fbi = {} + for _, f in ipairs(desc.fields) do fbi[f.id] = f end + desc.field_by_id = fbi + if desc.oneofs then + for _, members in pairs(desc.oneofs) do + for _, fname in ipairs(members) do + for _, f in ipairs(desc.fields) do + if f.name == fname then + local sibs = {} + for _, other in ipairs(members) do + if other ~= fname then sibs[#sibs + 1] = other end + end + f.oneof_siblings = sibs + break + end + end + end + end + end + end + + -- 4) Emit wrapper functions per message. + for _, m in ipairs(msgs) do + local desc = msg_descs[m.full_name] + local flat = flat_name(m.full_name, pkg) + out[flat .. '_new'] = function(t) return t or {} end + out[flat .. '_encode'] = function(t) return codec.encode(desc, t) end + out[flat .. '_decode'] = function(b) return codec.decode(desc, b) end + -- Optional accessors + for _, f in ipairs(desc.fields) do + if f.optional then + out[flat .. '_has_' .. f.name] = function(t) return t[f.name] ~= nil end + out[flat .. '_clear_' .. f.name] = function(t) t[f.name] = nil end + end + end + end + + return out +end + +return M diff --git a/runtime/pb/grpc.lua b/runtime/pb/grpc.lua new file mode 100644 index 0000000000000000000000000000000000000000..83b5eb641b6041586faa8c3515720e8ca7b858a8 --- /dev/null +++ b/runtime/pb/grpc.lua @@ -0,0 +1,302 @@ +-- gRPC transport interface + reference loopback / multiplex implementations. +-- +-- # Transport contract +-- +-- Unary: +-- transport:unary(path, req_bytes, ctx) -> resp_bytes +-- +-- Streaming (three flavors). Each returns a `client_view` stream object +-- — see "Stream object" below. +-- transport:server_stream(path, req_bytes, ctx) -> stream +-- transport:client_stream(path, ctx) -> stream +-- transport:bidi(path, ctx) -> stream +-- +-- # Stream object (client view) +-- +-- Methods that may be called by the caller side (the gRPC client): +-- stream:send(bytes) -- push a message (client_stream, bidi) +-- stream:close_send() -- signal "no more outgoing messages" +-- stream:recv() -> bytes, err -- pull next reply; (nil, err_or_nil) ends +-- stream:cancel() -- abort the call, drop pending messages +-- +-- For server_stream, `send` and `close_send` are no-ops (initial request +-- is conveyed via the call's req_bytes argument). +-- +-- # Server-side stream view +-- +-- Generated server code passes a "server_view" object to the user handler: +-- server_view:send(bytes) -- push a reply (server_stream, bidi) +-- server_view:recv() -> bytes, err -- pull next request (client_stream, bidi) +-- +-- The handler signals end-of-stream by returning. Errors thrown via +-- `error(...)` propagate to the client as the `err` returned by `recv()`. +-- +-- # Real transports +-- +-- HTTP/2, net.box-tunneled, IProto — those live in separate packages. +-- This module ships only loopback + multiplex, intended for tests and +-- in-process apps. +local fiber = require('fiber') + +local M = {} + +-- Default channel buffer size for in-process streams. Senders block when +-- the buffer is full; receivers block when it's empty. 16 messages is a +-- compromise between sender/receiver decoupling and memory footprint for +-- payload backlog. Override per call site via new_stream_pair(buf_size). +local DEFAULT_BUFFER = 16 + +-- Internal: shared state between the two stream views. +local function new_state() + return { + -- Recorded by server-side fiber when handler raises. Surfaced to + -- client via the `err` return of recv() once the reply channel + -- drains. + server_err = nil, + -- Set by client cancel(); the server side checks this on send and + -- treats it as an abort signal. + canceled = false, + } +end + +-- new_stream_pair(buf_size?) -> client_view, server_view, internal_state +-- +-- 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. +function M.new_stream_pair(buf_size) + buf_size = buf_size or DEFAULT_BUFFER + local c2s = fiber.channel(buf_size) -- client -> server + local s2c = fiber.channel(buf_size) -- server -> client + local state = new_state() + + -- Client-facing view + local client = {} + + function client:send(bytes) + if c2s:is_closed() then + error('pb.grpc: send after close_send', 0) + end + if state.canceled then + error('pb.grpc: stream canceled', 0) + end + c2s:put(bytes) + end + + function client:close_send() + if not c2s:is_closed() then c2s:close() end + end + + function client:recv() + local b = s2c:get() + if b == nil then + -- Either drained naturally or a server-side error closed it. + return nil, state.server_err + end + return b, nil + end + + function client:cancel() + state.canceled = true + if not c2s:is_closed() then c2s:close() end + -- We deliberately do NOT close s2c here. The handler fiber may + -- still write to it; closing under their feet would raise. Drain + -- on the next recv (which will see `canceled`). + end + + -- Server-facing view (used by the handler running on a worker fiber) + local server = {} + + function server:recv() + if state.canceled then return nil, 'canceled' end + local b = c2s:get() + if b == nil then return nil, nil end + return b, nil + end + + function server:send(bytes) + if state.canceled then + -- Caller gave up — silently drop, don't error in the handler. + return false + end + if s2c:is_closed() then return false end + s2c:put(bytes) + return true + end + + -- Internal: invoked by the transport, not by user code. + function server:_finish(err) + if err ~= nil then state.server_err = tostring(err) end + if not s2c:is_closed() then s2c:close() end + end + + function server:_force_close_recv() + -- Used by server_stream call where there's no client->server + -- channel; closing c2s up-front means server:recv() returns nil + -- immediately if (erroneously) called. + if not c2s:is_closed() then c2s:close() end + end + + return client, server, state +end + +-- --------------------------------------------------------------------------- +-- Internal helpers used by loopback + multiplex +-- --------------------------------------------------------------------------- + +local function dispatch_stream(streams, path, kind, req_bytes, ctx) + local entry = streams and streams[path] + if entry == nil then + error(('pb.grpc: no streaming method registered for %q'):format(path), 0) + end + if entry.kind ~= kind then + error(('pb.grpc: method %q is %s, called as %s') + :format(path, entry.kind, kind), 0) + end + + local client_view, server_view = M.new_stream_pair() + if kind == 'server_stream' then + -- No client->server messages after the initial request. + server_view:_force_close_recv() + client_view:close_send() + end + + fiber.create(function() + local ok, err = pcall(entry.handler, req_bytes, server_view, ctx or {}) + if ok then + server_view:_finish(nil) + else + server_view:_finish(err) + end + end) + + return client_view +end + +local function dispatch_unary(methods, path, req_bytes, ctx) + local handler = methods and methods[path] + if handler == nil then + error(('pb.grpc: unknown unary method %q'):format(path), 0) + end + return handler(req_bytes, ctx or {}) +end + +-- --------------------------------------------------------------------------- +-- Public transports +-- --------------------------------------------------------------------------- + +-- 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. +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) + end + local methods = server.methods + local streams = server.streams or {} + return { + unary = function(_, path, req_bytes, ctx) + return dispatch_unary(methods, path, req_bytes, ctx) + end, + server_stream = function(_, path, req_bytes, ctx) + return dispatch_stream(streams, path, 'server_stream', req_bytes, ctx) + end, + client_stream = function(_, path, ctx) + return dispatch_stream(streams, path, 'client_stream', nil, ctx) + end, + bidi = function(_, path, ctx) + return dispatch_stream(streams, path, 'bidi', nil, ctx) + end, + } +end + +-- multiplex({server1, server2, ...}) merges several servers' methods + +-- streams under a single transport. Errors on duplicate paths. +function M.multiplex(servers) + local methods, streams = {}, {} + for _, srv in ipairs(servers) do + for path, handler in pairs(srv.methods or {}) do + if methods[path] ~= nil then + error(("pb.grpc.multiplex: duplicate unary route %q"):format(path), 0) + end + methods[path] = handler + end + for path, entry in pairs(srv.streams or {}) do + if streams[path] ~= nil then + error(("pb.grpc.multiplex: duplicate streaming route %q"):format(path), 0) + end + streams[path] = entry + end + end + return { + unary = function(_, path, req_bytes, ctx) + return dispatch_unary(methods, path, req_bytes, ctx) + end, + server_stream = function(_, path, req_bytes, ctx) + return dispatch_stream(streams, path, 'server_stream', req_bytes, ctx) + end, + client_stream = function(_, path, ctx) + return dispatch_stream(streams, path, 'client_stream', nil, ctx) + end, + bidi = function(_, path, ctx) + return dispatch_stream(streams, path, 'bidi', nil, ctx) + end, + } +end + +-- --------------------------------------------------------------------------- +-- Helpers used by generated client code +-- --------------------------------------------------------------------------- +-- +-- These wrap a transport-level (bytes) stream in a typed (decoded +-- 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. +function M.wrap_server_stream(raw, output_decode) + return { + recv = function(_) + local bytes, err = raw:recv() + if bytes == nil then return nil, err end + return output_decode(bytes), nil + end, + cancel = function(_) raw:cancel() end, + } +end + +-- Wrap a client-streaming or bidi call: caller sends + recvs. +function M.wrap_call(raw, input_encode, output_decode) + return { + send = function(_, msg) + raw:send(input_encode(msg)) + end, + close_send = function(_) raw:close_send() end, + recv = function(_) + local bytes, err = raw:recv() + if bytes == nil then return nil, err end + return output_decode(bytes), nil + end, + cancel = function(_) raw:cancel() end, + } +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. +function M.wrap_server_view(raw, input_decode, output_encode) + local wrapped = {} + if input_decode ~= nil then + function wrapped:recv() + local bytes, err = raw:recv() + if bytes == nil then return nil, err end + return input_decode(bytes), nil + end + end + if output_encode ~= nil then + function wrapped:send(msg) raw:send(output_encode(msg)) end + end + return wrapped +end + +return M diff --git a/runtime/pb/init.lua b/runtime/pb/init.lua new file mode 100644 index 0000000000000000000000000000000000000000..1b0754d8776247dce513416673c89744e00c1d62 --- /dev/null +++ b/runtime/pb/init.lua @@ -0,0 +1,109 @@ +-- Public surface of the protobuf runtime used by generated code. +-- +-- Named `pb` (rather than `protobuf`) to avoid colliding with Tarantool's +-- built-in encode-only `require('protobuf')` module. +-- +-- Generated `_pb.lua` modules do: +-- local pb = require('pb') +-- ... +-- M.Person_encode = function(t) return pb.encode(M.Person_descriptor, t) end +-- M.Person_decode = function(b) return pb.decode(M.Person_descriptor, b) end +local codec = require('pb.codec') +local wire = require('pb.wire') +local wkt = require('pb.wkt') +local grpc = require('pb.grpc') +local parser = require('pb.parser') +local dynamic = require('pb.dynamic') +local pbjson = require('pb.json') + +return { + -- High-level codec + encode = codec.encode, + decode = codec.decode, + + -- Wire-format primitives (exposed for advanced users / tests) + wire = wire, + + -- Well-known types (google.protobuf.*) — see runtime/pb/wkt.lua. + wkt = wkt, + + -- Sentinel for google.protobuf.Value's null_value / JSON null. + NULL = wkt.NULL, + + -- Type registry (used by google.protobuf.Any pack/unpack). + register = wkt.register, + lookup = wkt.lookup, + any = { + pack = wkt.any_pack, + unpack = wkt.any_unpack, + }, + + -- gRPC transport interface + loopback — see runtime/pb/grpc.lua. + grpc = grpc, + + -- Runtime .proto parsing: build a module from a .proto source string. + -- + -- local hello = pb.parse(io.open('hello.proto'):read('*a')) + -- local bytes = hello.Person_encode({name = 'Alice'}) + -- + -- Output shape mirrors what protoc-gen-tarantool emits in `mode=runtime`. + parse = function(source) return dynamic.build(parser.parse(source)) end, + + -- Low-level access for advanced use. + parser = parser, + dynamic = dynamic, + + -- proto3 JSON (canonical mapping). pb.json.encode(desc, t) -> string; + -- pb.json.decode(desc, s) -> table. + json = pbjson, + + -- Wire type constants + WIRE_VARINT = wire.WIRE_VARINT, + WIRE_I64 = wire.WIRE_I64, + WIRE_LEN = wire.WIRE_LEN, + WIRE_I32 = wire.WIRE_I32, + + -- Helpers for working with cdata 64-bit ints from outside. + to_uint64 = wire.to_uint64, + to_int64 = wire.to_int64, + + -- Helper for building enum descriptors at codegen time. + enum = function(name, values) + local by_name, by_value = {}, {} + for k, v in pairs(values) do + by_name[k] = v + by_value[v] = k + end + return {name = name, by_name = by_name, by_value = by_value} + end, + + -- Helper for finalizing a message descriptor: fills in field_by_id from fields[]. + -- Generated code calls this after constructing the fields table so cross-references + -- (including self-references) can be patched in before sealing. + finalize_message = function(desc) + local fbi = {} + for _, f in ipairs(desc.fields) do fbi[f.id] = f end + desc.field_by_id = fbi + -- Pre-compute sibling lists for each oneof field so decode can clear + -- them in O(k) without rescanning. + if desc.oneofs then + for oname, members in pairs(desc.oneofs) do + for _, fname in ipairs(members) do + local f = nil + for _, fld in ipairs(desc.fields) do + if fld.name == fname then f = fld; break end + end + if f then + local sibs = {} + for _, other in ipairs(members) do + if other ~= fname then sibs[#sibs + 1] = other end + end + f.oneof_siblings = sibs + end + _ = oname + end + end + end + return desc + end, +} diff --git a/runtime/pb/json.lua b/runtime/pb/json.lua new file mode 100644 index 0000000000000000000000000000000000000000..456266999c60d54f08baf1dc30fdaa907074184c --- /dev/null +++ b/runtime/pb/json.lua @@ -0,0 +1,544 @@ +-- proto3 JSON encoding (canonical mapping). +-- +-- Spec: https://protobuf.dev/programming-guides/proto3/#json +-- +-- Highlights of how we map types both ways: +-- * 32-bit ints / float / double / bool -> JSON number/bool +-- * 64-bit ints (int64/uint64/sint64/fixed64/sfixed64) -> JSON string +-- (JSON doubles lose precision past 2^53; the spec mandates strings) +-- * bytes -> base64 string +-- * enum -> string name when known, else number +-- * message -> JSON object (camelCase keys) +-- * map -> JSON object (keys stringified per spec) +-- * Timestamp -> ISO 8601 "YYYY-MM-DDTHH:MM:SS[.nnnnnnnnn]Z" +-- * Duration -> "s" (decimal seconds with up to 9 fractional digits) +-- * Empty -> {} +-- * Wrapper messages -> unwrapped scalar +-- +-- Field names: emitted camelCase per spec; decoder accepts both camelCase +-- and the original snake_case so users aren't punished for either convention. +local ffi = require('ffi') +local json = require('json') +local digest = require('digest') +local datetime = require('datetime') +local wire = require('pb.wire') +local pbwkt = require('pb.wkt') + +local M = {} + +local TYPE_INFO = wire.TYPE_INFO +local INT64_FAMILY = {int64=true, uint64=true, sint64=true, fixed64=true, sfixed64=true} + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +-- snake_case -> camelCase (proto field naming convention for JSON). +local function to_camel(name) + return (name:gsub('_(%w)', function(c) return c:upper() end)) +end + +-- Lossless stringification of an integer/cdata for JSON output. +local function int_to_string(v) + if type(v) == 'cdata' then + return tostring(v):gsub('U?LL$', '') + end + return tostring(v) +end + +-- Convert a JSON string/number back to int64 or uint64 cdata. +local function string_to_int64(v, is_unsigned) + if type(v) == 'number' then v = string.format('%.0f', v) end + if type(v) ~= 'string' then error('expected JSON string or number for int64', 0) end + local cdata = tonumber64(v) + if cdata == nil then error('invalid int64 string: ' .. v, 0) end + if is_unsigned then return ffi.cast('uint64_t', cdata) end + return ffi.cast('int64_t', cdata) +end + +-- --------------------------------------------------------------------------- +-- Encode (proto-Lua table -> Lua table suitable for json.encode) +-- --------------------------------------------------------------------------- + +local to_json_value -- forward +local encode_message -- forward + +-- Encode a single scalar (not repeated/map). Returns a JSON-friendly value. +local function encode_scalar(proto_type, v) + if INT64_FAMILY[proto_type] then return int_to_string(v) end + if proto_type == 'uint32' then + -- Lua double can hold 0..2^32 - 1; emit as number. + return v + end + if proto_type == 'bytes' then return digest.base64_encode(v) end + if proto_type == 'float' or proto_type == 'double' then + -- JSON has no NaN/Infinity literals; spec mandates string sentinels. + if v ~= v then return 'NaN' end + if v == math.huge then return 'Infinity' end + if v == -math.huge then return '-Infinity' end + return v + end + return v -- string, bool, int32, sint32, fixed32, sfixed32 +end + +local function encode_enum(enum_desc, v) + if type(v) == 'string' then return v end + local name = enum_desc.by_value[v] + return name or v -- unknown numeric value: emit as number +end + +-- Convert a single proto value to a JSON-encodable value, based on field kind. +local function encode_field_value(field, v) + local kind = field.kind + if kind == 'scalar' then + return encode_scalar(field.proto_type, v) + elseif kind == 'enum' then + return encode_enum(field.enum, v) + elseif kind == 'message' then + return encode_message(field.message, v) + end + error('encode_field_value: unknown kind ' .. tostring(kind), 0) +end + +-- Map key encoding: per spec, all keys are strings in JSON output. +local function encode_map_key(key_field, k) + local pt = key_field.proto_type + if pt == 'bool' then return k and 'true' or 'false' end + if INT64_FAMILY[pt] then return int_to_string(k) end + if pt == 'string' then return k end + return tostring(k) -- int32/uint32/sint32/etc. +end + +-- Struct/Value/ListValue JSON mappings. +-- +-- Per the proto3 JSON spec: +-- * Struct ↔ JSON object (the `fields` map is hoisted away) +-- * ListValue ↔ JSON array (the `values` array is hoisted away) +-- * Value ↔ any JSON value (null/number/string/bool/object/array) +-- +-- Lua representations mirror what runtime/pb/wkt.lua produces: +-- * box.NULL → JSON null +-- * boolean/number/string → JSON true|false/number/string +-- * table tagged with pb.wkt.list → JSON array; otherwise → JSON object + +local PB_NULL = pbwkt.NULL + +local value_to_json, value_to_json_struct, value_to_json_list -- forwards + +value_to_json = function(v) + if v == nil or v == PB_NULL then return box.NULL end + local ty = type(v) + if ty == 'boolean' or ty == 'number' or ty == 'string' then return v end + if ty == 'cdata' then return tonumber(v) end + if ty == 'table' then + local mt = getmetatable(v) + if mt and mt.__pb_kind == 'list' then return value_to_json_list(v) end + if mt and mt.__pb_kind == 'struct' then return value_to_json_struct(v) end + if v[1] ~= nil then return value_to_json_list(v) end + return value_to_json_struct(v) + end + error('Value JSON: unsupported Lua type ' .. ty, 0) +end + +value_to_json_struct = function(t) + if t == nil then return setmetatable({}, {__serialize='map'}) end + local out, empty = {}, true + for k, v in pairs(t) do + out[tostring(k)] = value_to_json(v) + empty = false + end + if empty then return setmetatable(out, {__serialize='map'}) end + return out +end + +value_to_json_list = function(t) + if t == nil then return setmetatable({}, {__serialize='seq'}) end + local out = {} + for i = 1, #t do out[i] = value_to_json(t[i]) end + return setmetatable(out, {__serialize='seq'}) +end + +-- FieldMask JSON: paths joined by `,`. snake_case → lowerCamelCase per spec. +local function fieldmask_to_json(v) + if v == nil or #v == 0 then return '' end + local parts = {} + for i = 1, #v do parts[i] = to_camel(v[i]) end + return table.concat(parts, ',') +end + +local function fieldmask_from_json(s) + if type(s) ~= 'string' or s == '' then return {} end + local out = {} + for part in (s .. ','):gmatch('([^,]+),') do + -- lowerCamelCase -> snake_case + out[#out + 1] = (part:gsub('(%u)', function(c) return '_' .. c:lower() end)) + end + return out +end + +-- Any JSON: a flat object {"@type": "", ...fields...}. Decoded by +-- consulting pb.wkt registry. Pack/unpack the payload through the registered +-- descriptor; unregistered types fall back to the opaque {type_url,value} form. +local function any_to_json(v) + if v == nil then return setmetatable({}, {__serialize='map'}) end + if type(v) ~= 'table' then + error('Any JSON: expected table, got ' .. type(v), 0) + end + local type_url = v.type_url or '' + local bytes = v.value or '' + local desc = pbwkt.lookup(type_url) + if desc == nil or bytes == '' then + -- Opaque fallback: emit the protobuf representation as-is so a round + -- trip is still possible without a registered descriptor. + local obj = {['@type'] = type_url} + if bytes ~= '' then obj.value = digest.base64_encode(bytes) end + return obj + end + local inner = desc.decode and desc.decode(bytes) + or require('pb.codec').decode(desc, bytes) + local payload = encode_message(desc, inner) + -- For Value/Struct/ListValue/wrappers the JSON form is not an object; the + -- spec says to nest under "value" then. + if type(payload) ~= 'table' or getmetatable(payload) and + getmetatable(payload).__serialize == 'seq' then + return {['@type'] = type_url, value = payload} + end + payload['@type'] = type_url + return payload +end + +-- WKT special-case encoders. Return either a JSON-encodable Lua value or +-- nil to indicate "no override; fall back to generic message walk". +local function encode_wkt(desc, v) + local name = desc.name + if name == 'google.protobuf.Empty' then + return setmetatable({}, {__serialize='map'}) -- emit as {} + end + if name == 'google.protobuf.Timestamp' then + local dt = v + if type(v) == 'table' then + dt = datetime.new({timestamp = tonumber(v.seconds or 0), + nsec = v.nanos or 0}) + elseif type(v) == 'cdata' and datetime.is_datetime(v) then + dt = v + elseif type(v) == 'number' then + dt = datetime.new({timestamp = math.floor(v), + nsec = math.floor((v - math.floor(v)) * 1e9 + 0.5)}) + else + error('Timestamp JSON: expected datetime/table/number', 0) + end + return tostring(dt):gsub('Z$', 'Z') -- Tarantool already emits ISO 8601 + end + if name == 'google.protobuf.Duration' then + local seconds, nanos + if type(v) == 'table' then + seconds = tonumber(v.seconds or 0) + nanos = v.nanos or 0 + elseif type(v) == 'number' then + seconds = math.floor(v) + nanos = math.floor((v - seconds) * 1e9 + 0.5) + else + error('Duration JSON: expected table or number', 0) + end + local total_secs = seconds + if nanos == 0 then return string.format('%ds', total_secs) end + local fraction = string.format('.%09d', nanos):gsub('0+$', '') + if fraction == '.' then fraction = '' end + return string.format('%d%ss', total_secs, fraction) + end + -- Wrappers: encode is just the unwrapped value. + local wrap = name:match('^google%.protobuf%.(%w+)Value$') + if wrap then + local wrapper_proto = { + Int32 = 'int32', UInt32 = 'uint32', Int64 = 'int64', UInt64 = 'uint64', + Float = 'float', Double = 'double', Bool = 'bool', + String = 'string', Bytes = 'bytes', + } + local pt = wrapper_proto[wrap] + if pt then return encode_scalar(pt, v) end + end + if name == 'google.protobuf.Struct' then + return value_to_json_struct(v) + end + if name == 'google.protobuf.ListValue' then + return value_to_json_list(v) + end + if name == 'google.protobuf.Value' then + return value_to_json(v) + end + if name == 'google.protobuf.FieldMask' then + return fieldmask_to_json(v) + end + if name == 'google.protobuf.Any' then + return any_to_json(v) + end + return nil +end + +encode_message = function(desc, t) + if t == nil then return nil end + local override = encode_wkt(desc, t) + if override ~= nil then return override end + + local out = {} + for _, f in ipairs(desc.fields) do + local v = t[f.name] + if v ~= nil then + local key = to_camel(f.name) + if f.kind == 'map' then + if next(v) ~= nil then + local obj = {} + for k, mv in pairs(v) do + obj[encode_map_key(f.key, k)] = encode_field_value(f.value, mv) + end + out[key] = obj + end + elseif f.repeated then + if #v > 0 then + local arr = {} + for i = 1, #v do arr[i] = encode_field_value(f, v[i]) end + out[key] = arr + end + else + -- proto3 default elision (unless presence is meaningful). + local emit = true + if not (f.optional or f.oneof) then + if f.kind == 'scalar' then + local pt = f.proto_type + if pt == 'string' or pt == 'bytes' then + if v == '' then emit = false end + elseif pt == 'bool' then + if v == false then emit = false end + else + -- numeric default + if v == 0 or (type(v) == 'cdata' and v == ffi.cast('int64_t', 0)) then + emit = false + end + end + elseif f.kind == 'enum' then + if v == 0 or v == f.enum.by_value[0] then emit = false end + end + end + if emit then out[key] = encode_field_value(f, v) end + end + end + end + return out +end + +to_json_value = encode_message + +function M.encode(desc, t) + return json.encode(encode_message(desc, t)) +end + +-- --------------------------------------------------------------------------- +-- Decode (JSON -> proto-Lua table) +-- --------------------------------------------------------------------------- + +local decode_message -- forward + +local function decode_scalar(proto_type, v) + if INT64_FAMILY[proto_type] then + return string_to_int64(v, proto_type:sub(1, 1) == 'u' or proto_type == 'fixed64') + end + if proto_type == 'bytes' then return digest.base64_decode(v) end + if proto_type == 'float' or proto_type == 'double' then + if v == 'NaN' then return 0/0 end + if v == 'Infinity' then return math.huge end + if v == '-Infinity' then return -math.huge end + if type(v) == 'string' then return tonumber(v) end + return v + end + if proto_type == 'bool' then + if type(v) == 'string' then return v == 'true' end + return v and true or false + end + -- 32-bit ints + string: accept JSON string or number defensively. + if type(v) == 'string' and proto_type ~= 'string' then return tonumber(v) end + return v +end + +local function decode_enum(enum_desc, v) + if type(v) == 'string' then + local n = enum_desc.by_name[v] + if n ~= nil then return n end + end + return tonumber(v) or v +end + +local function decode_field_value(field, v) + local kind = field.kind + if kind == 'scalar' then return decode_scalar(field.proto_type, v) + elseif kind == 'enum' then return decode_enum(field.enum, v) + elseif kind == 'message' then return decode_message(field.message, v) end + error('decode_field_value: unknown kind ' .. tostring(kind), 0) +end + +local function decode_map_key(key_field, k) + local pt = key_field.proto_type + if pt == 'string' then return k end + if pt == 'bool' then return k == 'true' end + if INT64_FAMILY[pt] then return string_to_int64(k, pt:sub(1, 1) == 'u' or pt == 'fixed64') end + return tonumber(k) +end + +local json_to_value, json_to_struct, json_to_list, json_to_any -- forwards + +json_to_any = function(v) + if v == nil then return {type_url = '', value = ''} end + if type(v) ~= 'table' then + error('Any JSON: expected object, got ' .. type(v), 0) + end + local type_url = v['@type'] or '' + local desc = pbwkt.lookup(type_url) + if desc == nil then + -- Opaque fallback (no registered descriptor): expect a base64 `value` + -- field, mirroring our encode-side fallback. + local raw = v.value + return {type_url = type_url, + value = raw and digest.base64_decode(raw) or ''} + end + -- Reconstruct the inner message from the flat JSON, skipping @type. + local payload = {} + for k, mv in pairs(v) do + if k ~= '@type' then payload[k] = mv end + end + -- For Value/Struct/ListValue/wrappers the spec nests under `value`. + if payload.value ~= nil and next(payload, next(payload)) == nil then + payload = payload.value + end + local inner = decode_message(desc, payload) + local bytes = desc.encode and desc.encode(inner) + or require('pb.codec').encode(desc, inner) + return {type_url = type_url, value = bytes} +end + + +json_to_value = function(v) + if v == nil or v == box.NULL then return PB_NULL end + local ty = type(v) + if ty == 'boolean' or ty == 'number' or ty == 'string' then return v end + if ty == 'cdata' then return tonumber(v) end + if ty == 'table' then + if v[1] ~= nil or next(v) == nil and getmetatable(v) + and getmetatable(v).__serialize == 'seq' then + return json_to_list(v) + end + -- Heuristic: integer-keyed → list, else → struct. + if v[1] ~= nil then return json_to_list(v) end + return json_to_struct(v) + end + error('Value JSON: unsupported type ' .. ty, 0) +end + +json_to_struct = function(v) + local out = pbwkt.struct({}) + if v == nil then return out end + for k, mv in pairs(v) do out[k] = json_to_value(mv) end + return out +end + +json_to_list = function(v) + local out = pbwkt.list({}) + if v == nil then return out end + for i = 1, #v do out[i] = json_to_value(v[i]) end + return out +end + +local function decode_wkt(desc, v) + local name = desc.name + if name == 'google.protobuf.Empty' then return {} end + if name == 'google.protobuf.Timestamp' then + return datetime.parse(v) + end + if name == 'google.protobuf.Duration' then + local body = v:gsub('s$', '') + local sign = 1 + if body:sub(1, 1) == '-' then sign = -1; body = body:sub(2) end + local sec_str, frac = body:match('^(%d+)%.?(%d*)$') + if sec_str == nil then error('invalid Duration JSON: ' .. v, 0) end + local nanos = 0 + if frac and frac ~= '' then + nanos = tonumber((frac .. '000000000'):sub(1, 9)) + end + return {seconds = sign * tonumber(sec_str), nanos = sign * nanos} + end + local wrap = name:match('^google%.protobuf%.(%w+)Value$') + if wrap then + local wrapper_proto = { + Int32 = 'int32', UInt32 = 'uint32', Int64 = 'int64', UInt64 = 'uint64', + Float = 'float', Double = 'double', Bool = 'bool', + String = 'string', Bytes = 'bytes', + } + local pt = wrapper_proto[wrap] + if pt then return decode_scalar(pt, v) end + end + if name == 'google.protobuf.Value' then + return json_to_value(v) + end + if name == 'google.protobuf.Struct' then + return json_to_struct(v) + end + if name == 'google.protobuf.ListValue' then + return json_to_list(v) + end + if name == 'google.protobuf.FieldMask' then + return fieldmask_from_json(v) + end + if name == 'google.protobuf.Any' then + return json_to_any(v) + end + return nil +end + +decode_message = function(desc, v) + if v == nil then return nil end + local override = decode_wkt(desc, v) + if override ~= nil then return override end + if type(v) ~= 'table' then + error('expected JSON object for ' .. desc.name .. ', got ' .. type(v), 0) + end + + -- Build a name -> field map covering both camelCase and snake_case. + local field_by_json_name = desc._json_field_by_name + if field_by_json_name == nil then + field_by_json_name = {} + for _, f in ipairs(desc.fields) do + field_by_json_name[f.name] = f + field_by_json_name[to_camel(f.name)] = f + end + desc._json_field_by_name = field_by_json_name + end + + local out = {} + for k, jv in pairs(v) do + local f = field_by_json_name[k] + if f ~= nil then + if f.kind == 'map' then + local m = {} + for mk, mv in pairs(jv) do + m[decode_map_key(f.key, mk)] = decode_field_value(f.value, mv) + end + out[f.name] = m + elseif f.repeated then + local arr = {} + for i = 1, #jv do arr[i] = decode_field_value(f, jv[i]) end + out[f.name] = arr + else + out[f.name] = decode_field_value(f, jv) + end + end + -- Unknown JSON keys are silently ignored (per spec). + end + return out +end + +function M.decode(desc, s) + return decode_message(desc, json.decode(s)) +end + +M.to_json_value = to_json_value +M.from_json_value = decode_message + +return M diff --git a/runtime/pb/parser.lua b/runtime/pb/parser.lua new file mode 100644 index 0000000000000000000000000000000000000000..8110776671b86397b656f7683b0929aba25ad1d0 --- /dev/null +++ b/runtime/pb/parser.lua @@ -0,0 +1,347 @@ +-- Pure-Lua .proto file parser (proto3). +-- +-- Adapted from tarantool-etcd/lib/protobuf/parser.lua with these changes: +-- * proto3 explicit `optional` is tracked (sets field.optional = true) +-- * the output AST is intentionally minimal — descriptor synthesis lives +-- in `pb.dynamic`, which converts the AST into the runtime descriptor +-- format used by `pb.encode`/`pb.decode`. +-- +-- Supported: +-- syntax = "proto3" +-- package, import (recorded but not resolved across files) +-- message + nested messages + nested enums + oneofs + maps +-- enum (top-level + nested) +-- service { rpc Method(In) returns (Out); } +-- proto3 explicit `optional` +-- +-- Not yet: +-- custom options past simple `option name = value;` (skipped) +-- extensions, reserved fields (skipped harmlessly) +local M = {} + +-- --------------------------------------------------------------------------- +-- Tokenizer +-- --------------------------------------------------------------------------- + +local function tokenize(source) + local tokens = {} + local pos = 1 + local len = #source + + while pos <= len do + local ws_start, ws_end = source:find('^%s+', pos) + if ws_start then pos = ws_end + 1 end + if pos > len then break end + + if source:sub(pos, pos + 1) == '//' then + local nl = source:find('\n', pos) + pos = nl and nl + 1 or len + 1 + elseif source:sub(pos, pos + 1) == '/*' then + local close = source:find('%*/', pos + 2) + pos = close and close + 2 or len + 1 + elseif source:sub(pos, pos) == '"' then + local str_end = pos + 1 + while str_end <= len do + local c = source:sub(str_end, str_end) + if c == '"' then break + elseif c == '\\' then str_end = str_end + 2 + else str_end = str_end + 1 end + end + tokens[#tokens + 1] = {type = 'string', value = source:sub(pos + 1, str_end - 1)} + pos = str_end + 1 + elseif source:match('^%-?%d', pos) then + local num_end = source:find('[^%d%.xXa-fA-FeE%+%-]', pos + 1) or len + 1 + tokens[#tokens + 1] = {type = 'number', value = source:sub(pos, num_end - 1)} + pos = num_end + elseif source:match('^[a-zA-Z_]', pos) then + local id_end = source:find('[^a-zA-Z0-9_.]', pos + 1) or len + 1 + tokens[#tokens + 1] = {type = 'ident', value = source:sub(pos, id_end - 1)} + pos = id_end + else + tokens[#tokens + 1] = {type = 'punct', value = source:sub(pos, pos)} + pos = pos + 1 + end + end + return tokens +end + +-- --------------------------------------------------------------------------- +-- Parser +-- --------------------------------------------------------------------------- + +local function parse(tokens) + local pos = 1 + local result = { + syntax = 'proto3', + package = '', + imports = {}, + messages = {}, -- declaration-ordered: messages[i] = {name=, ...} + enums = {}, -- declaration-ordered: enums[i] = {name=, values=} + services = {}, + } + + local function peek() return tokens[pos] end + local function consume(typ, val) + local tok = tokens[pos] + if not tok then error("protobuf parser: unexpected end of input", 0) end + if typ and tok.type ~= typ then + error(("protobuf parser: expected %s, got %s (%q) at token %d") + :format(typ, tok.type, tok.value or '', pos), 0) + end + if val and tok.value ~= val then + error(("protobuf parser: expected %q, got %q at token %d") + :format(val, tok.value, pos), 0) + end + pos = pos + 1 + return tok + end + local function match(typ, val) + local tok = peek() + if tok and tok.type == typ and (val == nil or tok.value == val) then + return consume() + end + return nil + end + + local function parse_option_value() + local tok = peek() + if tok.type == 'string' then return consume().value + elseif tok.type == 'number' then return tonumber(consume().value) + elseif tok.type == 'ident' then + local v = consume().value + if v == 'true' then return true end + if v == 'false' then return false end + return v + end + error("protobuf parser: invalid option value", 0) + end + + local function parse_field_options() + local options = {} + if match('punct', '[') then + repeat + local name = consume('ident').value + consume('punct', '=') + options[name] = parse_option_value() + until not match('punct', ',') + consume('punct', ']') + end + return options + end + + local function skip_to_semi() + while not match('punct', ';') do consume() end + end + + local function parse_enum() + consume('ident', 'enum') + local name = consume('ident').value + consume('punct', '{') + local enum = {name = name, values = {}} + while not match('punct', '}') do + if match('ident', 'option') or match('ident', 'reserved') then + skip_to_semi() + else + local value_name = consume('ident').value + consume('punct', '=') + local value_num = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + enum.values[value_name] = value_num + end + end + return enum + end + + local function parse_message() + consume('ident', 'message') + local name = consume('ident').value + consume('punct', '{') + local message = { + name = name, + fields = {}, -- declaration-ordered + nested_messages = {}, + nested_enums = {}, + oneofs = {}, -- ordered: {name=, fields={}} + } + + local function emit_field(field) + message.fields[#message.fields + 1] = field + end + + while not match('punct', '}') do + local tok = peek() + + if tok.type == 'ident' and tok.value == 'message' then + local nested = parse_message() + message.nested_messages[#message.nested_messages + 1] = nested + elseif tok.type == 'ident' and tok.value == 'enum' then + local nested = parse_enum() + message.nested_enums[#message.nested_enums + 1] = nested + elseif tok.type == 'ident' and tok.value == 'oneof' then + consume('ident', 'oneof') + local oneof_name = consume('ident').value + consume('punct', '{') + local oneof_fields = {} + while not match('punct', '}') do + local ft = consume('ident').value + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + oneof_fields[#oneof_fields + 1] = fn + emit_field({name = fn, type = ft, id = fid, oneof = oneof_name}) + end + message.oneofs[#message.oneofs + 1] = {name = oneof_name, fields = oneof_fields} + elseif tok.type == 'ident' and tok.value == 'reserved' then + consume() + skip_to_semi() + elseif tok.type == 'ident' and tok.value == 'option' then + consume() + skip_to_semi() + elseif tok.type == 'ident' and tok.value == 'extensions' then + consume() + skip_to_semi() + elseif tok.type == 'ident' and tok.value == 'optional' then + -- proto3 explicit optional + consume() + local ft = consume('ident').value + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + emit_field({name = fn, type = ft, id = fid, optional = true}) + elseif tok.type == 'ident' and tok.value == 'required' then + -- Proto2-style required: tolerate by treating as a plain field. + consume() + local ft = consume('ident').value + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + emit_field({name = fn, type = ft, id = fid}) + elseif tok.type == 'ident' and tok.value == 'repeated' then + consume() + local ft = consume('ident').value + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + local opts = parse_field_options() + consume('punct', ';') + emit_field({ + name = fn, type = ft, id = fid, repeated = true, + packed = opts.packed, + }) + elseif tok.type == 'ident' and tok.value == 'map' then + consume() + consume('punct', '<') + local key_type = consume('ident').value + consume('punct', ',') + local value_type = consume('ident').value + consume('punct', '>') + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + emit_field({ + name = fn, id = fid, kind = 'map', + key_type = key_type, value_type = value_type, + }) + elseif tok.type == 'ident' then + local ft = consume('ident').value + local fn = consume('ident').value + consume('punct', '=') + local fid = tonumber(consume('number').value) + parse_field_options() + consume('punct', ';') + emit_field({name = fn, type = ft, id = fid}) + else + error(("protobuf parser: unexpected token %q in message %s") + :format(tok.value, name), 0) + end + end + return message + end + + local function parse_service() + consume('ident', 'service') + local name = consume('ident').value + consume('punct', '{') + local svc = {name = name, methods = {}} + while not match('punct', '}') do + if match('ident', 'option') then + skip_to_semi() + elseif match('ident', 'rpc') then + local method_name = consume('ident').value + consume('punct', '(') + local client_streaming = match('ident', 'stream') ~= nil + local input = consume('ident').value + consume('punct', ')') + consume('ident', 'returns') + consume('punct', '(') + local server_streaming = match('ident', 'stream') ~= nil + local output = consume('ident').value + consume('punct', ')') + if match('punct', '{') then + while not match('punct', '}') do consume() end + else + consume('punct', ';') + end + svc.methods[#svc.methods + 1] = { + name = method_name, + input = input, + output = output, + client_streaming = client_streaming, + server_streaming = server_streaming, + } + else + consume() + end + end + return svc + end + + while pos <= #tokens do + local tok = peek() + if not tok then break end + + if tok.type == 'ident' and tok.value == 'syntax' then + consume() + consume('punct', '=') + result.syntax = consume('string').value + consume('punct', ';') + elseif tok.type == 'ident' and tok.value == 'package' then + consume() + result.package = consume('ident').value + consume('punct', ';') + elseif tok.type == 'ident' and tok.value == 'import' then + consume() + match('ident', 'public') + match('ident', 'weak') + local path = consume('string').value + result.imports[#result.imports + 1] = path + consume('punct', ';') + elseif tok.type == 'ident' and tok.value == 'option' then + consume() + skip_to_semi() + elseif tok.type == 'ident' and tok.value == 'message' then + result.messages[#result.messages + 1] = parse_message() + elseif tok.type == 'ident' and tok.value == 'enum' then + result.enums[#result.enums + 1] = parse_enum() + elseif tok.type == 'ident' and tok.value == 'service' then + result.services[#result.services + 1] = parse_service() + else + consume() -- skip unknown top-level constructs + end + end + return result +end + +function M.tokenize(source) return tokenize(source) end +function M.parse(source) return parse(tokenize(source)) end + +return M diff --git a/runtime/pb/wire.lua b/runtime/pb/wire.lua new file mode 100644 index 0000000000000000000000000000000000000000..592d203f1a31b42dc53f000ea933a4144c31cd74 --- /dev/null +++ b/runtime/pb/wire.lua @@ -0,0 +1,368 @@ +-- Low-level protobuf wire format (proto3). +-- Pure Lua + LuaJIT FFI; no Tarantool-specific dependencies. +local ffi = require('ffi') +local bit = require('bit') + +local M = {} + +-- Wire type constants (https://protobuf.dev/programming-guides/encoding/#structure) +M.WIRE_VARINT = 0 +M.WIRE_I64 = 1 +M.WIRE_LEN = 2 +M.WIRE_I32 = 5 + +local UINT64 = ffi.typeof('uint64_t') +local INT64 = ffi.typeof('int64_t') +local UINT64_ZERO = UINT64(0) +local CONT_MASK = UINT64(bit.bnot(0x7f)) -- 0xFFFFFFFFFFFFFF80 + +-- Coerce any integer-like value to uint64_t cdata. +-- Negative Lua numbers are sign-extended via int64_t (protobuf wire spec). +local function to_uint64(v) + local t = type(v) + if t == 'number' then + if v < 0 then return UINT64(INT64(v)) end + return UINT64(v) + elseif t == 'cdata' then + return UINT64(v) + elseif t == 'boolean' then + return v and UINT64(1) or UINT64_ZERO + end + error("cannot coerce " .. t .. " to uint64", 0) +end +M.to_uint64 = to_uint64 + +local function to_int64(v) + local t = type(v) + if t == 'number' or t == 'cdata' then return INT64(v) end + error("cannot coerce " .. t .. " to int64", 0) +end +M.to_int64 = to_int64 + +-- --------------------------------------------------------------------------- +-- Varint +-- --------------------------------------------------------------------------- + +-- encode_varint(n) -> string +-- Accepts uint64_t/int64_t cdata, Lua number, or boolean. +local function encode_varint(n) + n = to_uint64(n) + local out = {} + local i = 1 + while bit.band(n, CONT_MASK) ~= UINT64_ZERO do + out[i] = string.char(tonumber(bit.bor(bit.band(n, 0x7f), 0x80))) + n = bit.rshift(n, 7) + i = i + 1 + end + out[i] = string.char(tonumber(n)) + return table.concat(out) +end +M.encode_varint = encode_varint + +-- decode_varint(buf, pos) -> uint64_t cdata, new_pos (1-based) +local function decode_varint(buf, pos) + local result = UINT64(0) + local shift = 0 + while true do + local b = buf:byte(pos) + if b == nil then error("truncated varint at offset " .. pos, 0) end + pos = pos + 1 + result = bit.bor(result, bit.lshift(UINT64(bit.band(b, 0x7f)), shift)) + if b < 0x80 then return result, pos end + shift = shift + 7 + if shift >= 70 then error("varint exceeds 10 bytes", 0) end + end +end +M.decode_varint = decode_varint + +-- --------------------------------------------------------------------------- +-- Tag +-- --------------------------------------------------------------------------- + +local function encode_tag(field_id, wire_type) + -- field_id < 2^29, fits in Lua double exactly. + return encode_varint(field_id * 8 + wire_type) +end +M.encode_tag = encode_tag + +local function decode_tag(buf, pos) + local v, npos = decode_varint(buf, pos) + v = tonumber(v) + return bit.rshift(v, 3), bit.band(v, 7), npos +end +M.decode_tag = decode_tag + +-- --------------------------------------------------------------------------- +-- ZigZag (sint32 / sint64) +-- --------------------------------------------------------------------------- + +-- 32-bit zigzag stays in Lua number range. +local function zigzag_encode32(n) + n = tonumber(n) + if n >= 0 then return n * 2 else return -n * 2 - 1 end +end +M.zigzag_encode32 = zigzag_encode32 + +local function zigzag_decode32(u) + u = tonumber(u) + if u % 2 == 0 then return u / 2 else return -((u + 1) / 2) end +end +M.zigzag_decode32 = zigzag_decode32 + +-- 64-bit zigzag uses cdata. +local function zigzag_encode64(n) + local i = to_int64(n) + local doubled = bit.lshift(UINT64(i), 1) + if i >= 0 then return doubled end + return bit.bnot(doubled) +end +M.zigzag_encode64 = zigzag_encode64 + +local function zigzag_decode64(u) + u = to_uint64(u) + local half = bit.rshift(u, 1) + if bit.band(u, 1) == UINT64_ZERO then return INT64(half) end + return INT64(bit.bnot(half)) +end +M.zigzag_decode64 = zigzag_decode64 + +-- --------------------------------------------------------------------------- +-- Fixed32 / Fixed64 (little-endian) +-- --------------------------------------------------------------------------- + +local function encode_fixed32(v) + -- Accepts uint32 (Lua number 0..2^32-1) or any integer cdata. + local u + if type(v) == 'cdata' then + u = tonumber(bit.band(UINT64(v), 0xffffffff)) + else + u = tonumber(v) + if u < 0 then u = u + 0x100000000 end + end + return string.char( + bit.band(u, 0xff), + bit.band(bit.rshift(u, 8), 0xff), + bit.band(bit.rshift(u, 16), 0xff), + bit.band(bit.rshift(u, 24), 0xff)) +end +M.encode_fixed32 = encode_fixed32 + +-- decode_fixed32(buf, pos) -> Lua number (0..2^32-1), new_pos +local function decode_fixed32(buf, pos) + local b1, b2, b3, b4 = buf:byte(pos, pos + 3) + if b4 == nil then error("truncated fixed32", 0) end + return b1 + b2 * 0x100 + b3 * 0x10000 + b4 * 0x1000000, pos + 4 +end +M.decode_fixed32 = decode_fixed32 + +local function encode_fixed64(v) + local u = to_uint64(v) + local lo = tonumber(bit.band(u, 0xffffffff)) + local hi = tonumber(bit.rshift(u, 32)) + return string.char( + bit.band(lo, 0xff), + bit.band(bit.rshift(lo, 8), 0xff), + bit.band(bit.rshift(lo, 16), 0xff), + bit.band(bit.rshift(lo, 24), 0xff), + bit.band(hi, 0xff), + bit.band(bit.rshift(hi, 8), 0xff), + bit.band(bit.rshift(hi, 16), 0xff), + bit.band(bit.rshift(hi, 24), 0xff)) +end +M.encode_fixed64 = encode_fixed64 + +-- decode_fixed64(buf, pos) -> uint64_t cdata, new_pos +local function decode_fixed64(buf, pos) + local b1, b2, b3, b4, b5, b6, b7, b8 = buf:byte(pos, pos + 7) + if b8 == nil then error("truncated fixed64", 0) end + local lo = b1 + b2 * 0x100 + b3 * 0x10000 + b4 * 0x1000000 + local hi = b5 + b6 * 0x100 + b7 * 0x10000 + b8 * 0x1000000 + return UINT64(lo) + bit.lshift(UINT64(hi), 32), pos + 8 +end +M.decode_fixed64 = decode_fixed64 + +-- --------------------------------------------------------------------------- +-- Float / Double (IEEE 754 little-endian) +-- --------------------------------------------------------------------------- + +ffi.cdef[[ + typedef union { float f; uint32_t u; uint8_t b[4]; } pb_f32_u_t; + typedef union { double d; uint64_t u; uint8_t b[8]; } pb_f64_u_t; +]] + +local F32 = ffi.new('pb_f32_u_t') +local F64 = ffi.new('pb_f64_u_t') + +local function encode_float(n) + F32.f = n + return ffi.string(F32.b, 4) +end +M.encode_float = encode_float + +local function decode_float(buf, pos) + if pos + 3 > #buf then error("truncated float", 0) end + ffi.copy(F32.b, buf:sub(pos, pos + 3), 4) + return tonumber(F32.f), pos + 4 +end +M.decode_float = decode_float + +local function encode_double(n) + F64.d = n + return ffi.string(F64.b, 8) +end +M.encode_double = encode_double + +local function decode_double(buf, pos) + if pos + 7 > #buf then error("truncated double", 0) end + ffi.copy(F64.b, buf:sub(pos, pos + 7), 8) + return tonumber(F64.d), pos + 8 +end +M.decode_double = decode_double + +-- --------------------------------------------------------------------------- +-- Length-delimited (LEN) +-- --------------------------------------------------------------------------- + +local function encode_len(s) + return encode_varint(#s) .. s +end +M.encode_len = encode_len + +-- decode_len(buf, pos) -> string, new_pos +local function decode_len(buf, pos) + local len, npos = decode_varint(buf, pos) + len = tonumber(len) + if npos + len - 1 > #buf then error("truncated LEN payload", 0) end + return buf:sub(npos, npos + len - 1), npos + len +end +M.decode_len = decode_len + +-- --------------------------------------------------------------------------- +-- Typed scalar encoders/decoders (one per proto3 scalar type). +-- +-- Both code paths (descriptor-driven runtime and inline codegen) call into +-- these. They take the application-side Lua value and produce/consume only +-- the value's wire bytes — the field tag is the caller's responsibility. +-- --------------------------------------------------------------------------- + +-- Encoders -------------------------------------------------------------------- +M.encode_int32 = encode_varint +M.encode_int64 = encode_varint +M.encode_uint32 = encode_varint +M.encode_uint64 = encode_varint + +local function encode_sint32(v) return encode_varint(zigzag_encode32(v)) end +local function encode_sint64(v) return encode_varint(zigzag_encode64(v)) end +local function encode_bool(v) return encode_varint(v and 1 or 0) end +M.encode_sint32 = encode_sint32 +M.encode_sint64 = encode_sint64 +M.encode_bool = encode_bool + +M.encode_sfixed32 = encode_fixed32 -- bits are identical, only interpretation differs +M.encode_sfixed64 = encode_fixed64 +M.encode_string = encode_len +M.encode_bytes = encode_len +-- (encode_fixed32, encode_fixed64, encode_float, encode_double already on M) + +-- Decoders -------------------------------------------------------------------- +local function decode_int32(buf, pos) + local u, np = decode_varint(buf, pos) + return tonumber(INT64(u)), np -- truncated to int32 range via int64 sign-extension +end +local function decode_int64(buf, pos) + local u, np = decode_varint(buf, pos) + return INT64(u), np +end +local function decode_uint32(buf, pos) + local u, np = decode_varint(buf, pos) + local n = tonumber(UINT64(u)) + if n < 0 then n = n + 0x100000000 end + return n, np +end +local function decode_uint64(buf, pos) + local u, np = decode_varint(buf, pos) + return UINT64(u), np +end +local function decode_sint32(buf, pos) + local u, np = decode_varint(buf, pos) + return zigzag_decode32(tonumber(u)), np +end +local function decode_sint64(buf, pos) + local u, np = decode_varint(buf, pos) + return zigzag_decode64(u), np +end +local function decode_bool(buf, pos) + local u, np = decode_varint(buf, pos) + return u ~= UINT64_ZERO, np +end +local function decode_sfixed32(buf, pos) + local n, np = decode_fixed32(buf, pos) + if n > 0x7fffffff then n = n - 0x100000000 end + return n, np +end +local function decode_sfixed64(buf, pos) + local u, np = decode_fixed64(buf, pos) + return INT64(u), np +end + +M.decode_int32 = decode_int32 +M.decode_int64 = decode_int64 +M.decode_uint32 = decode_uint32 +M.decode_uint64 = decode_uint64 +M.decode_sint32 = decode_sint32 +M.decode_sint64 = decode_sint64 +M.decode_bool = decode_bool +M.decode_sfixed32 = decode_sfixed32 +M.decode_sfixed64 = decode_sfixed64 +M.decode_string = decode_len +M.decode_bytes = decode_len +-- (decode_fixed32, decode_fixed64, decode_float, decode_double already on M +-- and have the right semantics for their proto types: fixed32 -> uint32 Lua +-- number 0..2^32-1, fixed64 -> uint64 cdata.) + +-- --------------------------------------------------------------------------- +-- TYPE_INFO — single-source-of-truth metadata for the codec layer and codegen. +-- Each entry carries the wire type, packed-list eligibility, and the typed +-- encode/decode functions defined above. Adapted from tarantool-etcd's +-- types.lua pattern. +-- --------------------------------------------------------------------------- + +M.TYPE_INFO = { + int32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_int32, decode = M.decode_int32 }, + int64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_int64, decode = M.decode_int64 }, + uint32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_uint32, decode = M.decode_uint32 }, + uint64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_uint64, decode = M.decode_uint64 }, + sint32 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_sint32, decode = M.decode_sint32 }, + sint64 = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_sint64, decode = M.decode_sint64 }, + bool = {wire = M.WIRE_VARINT, packable = true, encode = M.encode_bool, decode = M.decode_bool }, + fixed32 = {wire = M.WIRE_I32, packable = true, encode = M.encode_fixed32, decode = M.decode_fixed32 }, + sfixed32 = {wire = M.WIRE_I32, packable = true, encode = M.encode_sfixed32, decode = M.decode_sfixed32}, + float = {wire = M.WIRE_I32, packable = true, encode = M.encode_float, decode = M.decode_float }, + fixed64 = {wire = M.WIRE_I64, packable = true, encode = M.encode_fixed64, decode = M.decode_fixed64 }, + sfixed64 = {wire = M.WIRE_I64, packable = true, encode = M.encode_sfixed64, decode = M.decode_sfixed64}, + double = {wire = M.WIRE_I64, packable = true, encode = M.encode_double, decode = M.decode_double }, + string = {wire = M.WIRE_LEN, packable = false, encode = M.encode_string, decode = M.decode_string }, + bytes = {wire = M.WIRE_LEN, packable = false, encode = M.encode_bytes, decode = M.decode_bytes }, +} + +-- --------------------------------------------------------------------------- +-- Skip an unknown field (used by decoder when an unrecognized id appears). +-- skip_field(buf, pos, wire_type) -> new_pos +-- --------------------------------------------------------------------------- +local function skip_field(buf, pos, wire_type) + if wire_type == M.WIRE_VARINT then + local _, npos = decode_varint(buf, pos) + return npos + elseif wire_type == M.WIRE_I64 then + return pos + 8 + elseif wire_type == M.WIRE_LEN then + local len, npos = decode_varint(buf, pos) + return npos + tonumber(len) + elseif wire_type == M.WIRE_I32 then + return pos + 4 + end + error("unknown wire type " .. tostring(wire_type), 0) +end +M.skip_field = skip_field + +return M diff --git a/runtime/pb/wkt.lua b/runtime/pb/wkt.lua new file mode 100644 index 0000000000000000000000000000000000000000..531d9db166b4f0b574028b1fa478dccf715aa795 --- /dev/null +++ b/runtime/pb/wkt.lua @@ -0,0 +1,566 @@ +-- Well-known types (WKT) — implementations of selected google.protobuf.* +-- messages with idiomatic Lua surfaces. +-- +-- Each WKT is exposed three ways for parity with generated user code: +-- pb.wkt._descriptor -- usable in field descriptors +-- pb.wkt._encode(v) -- value -> wire bytes (no tag/len prefix) +-- pb.wkt._decode(buf) -- wire bytes -> value +-- +-- Descriptors carry custom .encode / .decode functions; the codec dispatches +-- to them in place of the generic message walk. +local ffi = require('ffi') +local datetime = require('datetime') +local wire = require('pb.wire') + +local M = {} + +-- --------------------------------------------------------------------------- +-- Helpers +-- --------------------------------------------------------------------------- + +local function emit_field_int64(out, n, tag, v) + if v ~= 0 and v ~= ffi.cast('int64_t', 0) then + out[n + 1] = tag + out[n + 2] = wire.encode_int64(v) + return n + 2 + end + return n +end + +local function emit_field_int32(out, n, tag, v) + if v ~= 0 then + out[n + 1] = tag + out[n + 2] = wire.encode_int32(v) + return n + 2 + end + return n +end + +-- --------------------------------------------------------------------------- +-- google.protobuf.Timestamp +-- message Timestamp { int64 seconds = 1; int32 nanos = 2; } +-- +-- Lua representation: +-- - encode accepts a `datetime` cdata, a {seconds=, nanos=} table, +-- or a Lua number (whole seconds; fractional part discarded). +-- - decode returns a `datetime` cdata. +-- --------------------------------------------------------------------------- + +local INT64_ZERO = ffi.cast('int64_t', 0) + +local function timestamp_to_parts(v) + if type(v) == 'cdata' and datetime.is_datetime(v) then + return ffi.cast('int64_t', v.epoch), v.nsec + elseif type(v) == 'table' then + return ffi.cast('int64_t', v.seconds or 0), v.nanos or 0 + elseif type(v) == 'number' then + local secs = math.floor(v) + return ffi.cast('int64_t', secs), math.floor((v - secs) * 1e9 + 0.5) + end + error("Timestamp: expected datetime, table {seconds,nanos}, or number; got " .. type(v), 0) +end + +local function timestamp_encode(v) + if v == nil then return '' end + local seconds, nanos = timestamp_to_parts(v) + local out, n = {}, 0 + n = emit_field_int64(out, n, '\x08', seconds) + n = emit_field_int32(out, n, '\x10', nanos) + return table.concat(out, '', 1, n) +end + +local function timestamp_decode(buf) + local seconds, nanos = INT64_ZERO, 0 + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + seconds, pos = wire.decode_int64(buf, pos) + elseif id == 2 then + nanos, pos = wire.decode_int32(buf, pos) + else + pos = wire.skip_field(buf, pos, wt) + end + end + return datetime.new({timestamp = tonumber(seconds), nsec = nanos}) +end + +M.Timestamp_encode = timestamp_encode +M.Timestamp_decode = timestamp_decode +M.Timestamp_descriptor = { + name = 'google.protobuf.Timestamp', + encode = timestamp_encode, + decode = timestamp_decode, +} + +-- --------------------------------------------------------------------------- +-- google.protobuf.Duration +-- message Duration { int64 seconds = 1; int32 nanos = 2; } +-- +-- Lua representation: {seconds=N, nanos=M} table (Tarantool's `interval` is +-- richer — months/days — and not a clean fit for raw seconds+nanos). +-- --------------------------------------------------------------------------- + +local function duration_encode(v) + if v == nil then return '' end + local seconds, nanos + if type(v) == 'table' then + seconds = ffi.cast('int64_t', v.seconds or 0) + nanos = v.nanos or 0 + elseif type(v) == 'number' then + local s = math.floor(v) + seconds = ffi.cast('int64_t', s) + nanos = math.floor((v - s) * 1e9 + 0.5) + else + error("Duration: expected table {seconds,nanos} or number, got " .. type(v), 0) + end + local out, n = {}, 0 + n = emit_field_int64(out, n, '\x08', seconds) + n = emit_field_int32(out, n, '\x10', nanos) + return table.concat(out, '', 1, n) +end + +local function duration_decode(buf) + local seconds, nanos = INT64_ZERO, 0 + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + seconds, pos = wire.decode_int64(buf, pos) + elseif id == 2 then + nanos, pos = wire.decode_int32(buf, pos) + else + pos = wire.skip_field(buf, pos, wt) + end + end + return {seconds = seconds, nanos = nanos} +end + +M.Duration_encode = duration_encode +M.Duration_decode = duration_decode +M.Duration_descriptor = { + name = 'google.protobuf.Duration', + encode = duration_encode, + decode = duration_decode, +} + +-- --------------------------------------------------------------------------- +-- google.protobuf.Empty +-- message Empty {} +-- --------------------------------------------------------------------------- + +local function empty_encode(_) return '' end +local function empty_decode(_) return {} end +M.Empty_encode = empty_encode +M.Empty_decode = empty_decode +M.Empty_descriptor = { + name = 'google.protobuf.Empty', + encode = empty_encode, + decode = empty_decode, +} + +-- --------------------------------------------------------------------------- +-- google.protobuf.Value wrappers +-- message Value { value = 1; } +-- +-- The Lua-side surface is the unwrapped value: encode accepts the raw +-- value, decode returns the raw value (default if missing). +-- --------------------------------------------------------------------------- + +-- For each wrapper type: {wire_type, encode_fn, decode_fn, default, default_check_fn} +local WRAPPERS = { + {'DoubleValue', wire.WIRE_I64, 'encode_double', 'decode_double', 0, function(v) return v == 0 end}, + {'FloatValue', wire.WIRE_I32, 'encode_float', 'decode_float', 0, function(v) return v == 0 end}, + {'Int64Value', wire.WIRE_VARINT, 'encode_int64', 'decode_int64', 0, function(v) return v == 0 or v == INT64_ZERO end}, + {'UInt64Value', wire.WIRE_VARINT, 'encode_uint64', 'decode_uint64', 0, function(v) return v == 0 or v == ffi.cast('uint64_t', 0) end}, + {'Int32Value', wire.WIRE_VARINT, 'encode_int32', 'decode_int32', 0, function(v) return v == 0 end}, + {'UInt32Value', wire.WIRE_VARINT, 'encode_uint32', 'decode_uint32', 0, function(v) return v == 0 end}, + {'BoolValue', wire.WIRE_VARINT, 'encode_bool', 'decode_bool', false,function(v) return v == false end}, + {'StringValue', wire.WIRE_LEN, 'encode_string', 'decode_string', '', function(v) return v == '' end}, + {'BytesValue', wire.WIRE_LEN, 'encode_bytes', 'decode_bytes', '', function(v) return v == '' end}, +} + +local TAG_BY_WIRE = { + [wire.WIRE_VARINT] = '\x08', -- field 1, VARINT + [wire.WIRE_I64] = '\x09', -- field 1, I64 + [wire.WIRE_LEN] = '\x0a', -- field 1, LEN + [wire.WIRE_I32] = '\x0d', -- field 1, I32 +} + +for _, spec in ipairs(WRAPPERS) do + local name, wt, enc_fn, dec_fn, default, is_default = unpack(spec) + local encode = wire[enc_fn] + local decode = wire[dec_fn] + local tag = TAG_BY_WIRE[wt] + + M[name .. '_encode'] = function(v) + if v == nil or is_default(v) then return '' end + return tag .. encode(v) + end + + M[name .. '_decode'] = function(buf) + if #buf == 0 then return default end + local pos, len = 1, #buf + local val = default + while pos <= len do + local id, wt2 + id, wt2, pos = wire.decode_tag(buf, pos) + if id == 1 then + val, pos = decode(buf, pos) + else + pos = wire.skip_field(buf, pos, wt2) + end + end + return val + end + + M[name .. '_descriptor'] = { + name = 'google.protobuf.' .. name, + encode = M[name .. '_encode'], + decode = M[name .. '_decode'], + } +end + +-- --------------------------------------------------------------------------- +-- google.protobuf.Struct / Value / ListValue +-- +-- message Value { +-- oneof kind { +-- NullValue null_value = 1; // varint enum +-- double number_value = 2; // I64 +-- string string_value = 3; // LEN +-- bool bool_value = 4; // varint +-- Struct struct_value = 5; // LEN +-- ListValue list_value = 6; // LEN +-- } +-- } +-- message Struct { map fields = 1; } +-- message ListValue { repeated Value values = 1; } +-- +-- Lua representation: +-- - `box.NULL` → null_value sentinel (re-exported as pb.NULL). +-- - boolean ↔ bool_value +-- - number ↔ number_value (proto double) +-- - string ↔ string_value +-- - table ↔ Struct (if hash-like or tagged via pb.wkt.struct) or +-- ListValue (if array-like / tagged via pb.wkt.list) +-- +-- Decode tags returned tables with hidden metatables so a Struct{}/ListValue{} +-- distinction survives an empty round trip; empty plain `{}` defaults to Struct. +-- --------------------------------------------------------------------------- + +local NULL = box.NULL +M.NULL = NULL + +local STRUCT_MT = {__pb_kind = 'struct'} +local LIST_MT = {__pb_kind = 'list'} + +local function struct_tag(t) return setmetatable(t or {}, STRUCT_MT) end +local function list_tag(t) return setmetatable(t or {}, LIST_MT) end +M.struct = struct_tag +M.list = list_tag + +local function is_list_like(t) + local mt = getmetatable(t) + if mt == LIST_MT then return true end + if mt == STRUCT_MT then return false end + return t[1] ~= nil -- empty {} → struct +end + +local value_encode, struct_encode, list_encode +local value_decode, struct_decode, list_decode + +value_encode = function(v) + if v == nil or v == NULL then return '\x08\x00' end -- field 1, varint 0 + local ty = type(v) + if ty == 'boolean' then + return '\x20' .. (v and '\x01' or '\x00') -- field 4, varint + end + if ty == 'number' then + return '\x11' .. wire.encode_double(v) -- field 2, I64 + end + if ty == 'string' then + return '\x1a' .. wire.encode_len(v) -- field 3, LEN + end + if ty == 'cdata' then + -- LuaJIT 64-bit ints get a double-precision approximation here. + return '\x11' .. wire.encode_double(tonumber(v)) + end + if ty == 'table' then + if is_list_like(v) then + return '\x32' .. wire.encode_len(list_encode(v)) -- field 6, LEN + end + return '\x2a' .. wire.encode_len(struct_encode(v)) -- field 5, LEN + end + error("Value: unsupported Lua type " .. ty, 0) +end + +struct_encode = function(t) + if t == nil then return '' end + local out, n = {}, 0 + -- Each Struct entry: tag(1, LEN)=0x0a, entry_len, entry_payload + -- Entry payload: tag(1, LEN)=0x0a + key_len_prefixed_bytes + -- + tag(2, LEN)=0x12 + value_len_prefixed_bytes + for k, v in pairs(t) do + local key_str = type(k) == 'string' and k or tostring(k) + local entry = '\x0a' .. wire.encode_len(key_str) + .. '\x12' .. wire.encode_len(value_encode(v)) + n = n + 1; out[n] = '\x0a' + n = n + 1; out[n] = wire.encode_len(entry) + end + return table.concat(out) +end + +list_encode = function(t) + if t == nil then return '' end + local out, n = {}, 0 + for i = 1, #t do + n = n + 1; out[n] = '\x0a' + n = n + 1; out[n] = wire.encode_len(value_encode(t[i])) + end + return table.concat(out) +end + +value_decode = function(buf) + -- Empty Value (no kind set on wire) → null per common impl convention. + if #buf == 0 then return NULL end + local pos, len = 1, #buf + local result = NULL -- last-set-wins; default to NULL if only unknowns + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local _u + _u, pos = wire.decode_varint(buf, pos) + result = NULL + elseif id == 2 then + local nv + nv, pos = wire.decode_double(buf, pos) + result = nv + elseif id == 3 then + local s + s, pos = wire.decode_string(buf, pos) + result = s + elseif id == 4 then + local b + b, pos = wire.decode_bool(buf, pos) + result = b + elseif id == 5 then + local payload + payload, pos = wire.decode_len(buf, pos) + result = struct_decode(payload) + elseif id == 6 then + local payload + payload, pos = wire.decode_len(buf, pos) + result = list_decode(payload) + else + pos = wire.skip_field(buf, pos, wt) + end + end + return result +end + +struct_decode = function(buf) + local result = setmetatable({}, STRUCT_MT) + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local payload + payload, pos = wire.decode_len(buf, pos) + local key, val = '', NULL + local ep, elim = 1, #payload + while ep <= elim do + local eid, ewt + eid, ewt, ep = wire.decode_tag(payload, ep) + if eid == 1 then + key, ep = wire.decode_string(payload, ep) + elseif eid == 2 then + local vbuf + vbuf, ep = wire.decode_len(payload, ep) + val = value_decode(vbuf) + else + ep = wire.skip_field(payload, ep, ewt) + end + end + result[key] = val + else + pos = wire.skip_field(buf, pos, wt) + end + end + return result +end + +list_decode = function(buf) + local result = setmetatable({}, LIST_MT) + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local payload + payload, pos = wire.decode_len(buf, pos) + result[#result + 1] = value_decode(payload) + else + pos = wire.skip_field(buf, pos, wt) + end + end + return result +end + +M.Value_encode = value_encode +M.Value_decode = value_decode +M.Value_descriptor = {name='google.protobuf.Value', encode=value_encode, decode=value_decode} + +M.Struct_encode = struct_encode +M.Struct_decode = struct_decode +M.Struct_descriptor = {name='google.protobuf.Struct', encode=struct_encode, decode=struct_decode} + +M.ListValue_encode = list_encode +M.ListValue_decode = list_decode +M.ListValue_descriptor = {name='google.protobuf.ListValue', encode=list_encode, decode=list_decode} + +-- --------------------------------------------------------------------------- +-- google.protobuf.Any +-- message Any { string type_url = 1; bytes value = 2; } +-- +-- Lua representation (opaque): +-- {type_url = 'type.googleapis.com/pkg.Msg', value = ''} +-- +-- The `pb.any.pack(desc, t)` / `pb.any.unpack(any_t)` helpers (see init.lua) +-- bridge the opaque form to user message tables via a process-global type +-- registry. They are optional — the opaque form round-trips on its own. +-- --------------------------------------------------------------------------- + +local any_encode, any_decode + +any_encode = function(v) + if v == nil then return '' end + if type(v) ~= 'table' then + error('Any: expected {type_url=,value=} table, got ' .. type(v), 0) + end + local out, n = {}, 0 + local type_url = v.type_url + if type_url ~= nil and type_url ~= '' then + n = n + 1; out[n] = '\x0a' -- field 1, LEN + n = n + 1; out[n] = wire.encode_len(type_url) + end + local value = v.value + if value ~= nil and value ~= '' then + n = n + 1; out[n] = '\x12' -- field 2, LEN + n = n + 1; out[n] = wire.encode_len(value) + end + return table.concat(out) +end + +any_decode = function(buf) + local type_url, value = '', '' + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + type_url, pos = wire.decode_string(buf, pos) + elseif id == 2 then + value, pos = wire.decode_bytes(buf, pos) + else + pos = wire.skip_field(buf, pos, wt) + end + end + return {type_url = type_url, value = value} +end + +M.Any_encode = any_encode +M.Any_decode = any_decode +M.Any_descriptor = {name='google.protobuf.Any', encode=any_encode, decode=any_decode} + +-- Per-process registry mapping type_url (or bare full name) to a message +-- descriptor. `pb.register(desc)` adds entries; pack/unpack look them up. +local REGISTRY = {} +M._registry = REGISTRY + +local DEFAULT_PREFIX = 'type.googleapis.com/' + +local function type_url_full_name(url) + return url:match('([^/]+)$') or url +end + +M.register = function(desc) + if type(desc) ~= 'table' or desc.name == nil then + error('pb.register: expected a descriptor with a `name` field', 0) + end + REGISTRY[desc.name] = desc + REGISTRY[DEFAULT_PREFIX .. desc.name] = desc + return desc +end + +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. +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) + end + local prefix = type_url_prefix or DEFAULT_PREFIX + local enc = desc.encode and desc.encode(t) + or require('pb.codec').encode(desc, t) + return {type_url = prefix .. desc.name, value = enc} +end + +-- Unpack an Any table. `desc_or_nil` overrides the registry lookup. +M.any_unpack = function(any_t, desc_or_nil) + if type(any_t) ~= 'table' then + error('pb.any.unpack: expected Any table', 0) + end + local desc = desc_or_nil or M.lookup(any_t.type_url or '') + if desc == nil then + error('pb.any.unpack: no descriptor for ' .. tostring(any_t.type_url), 0) + end + if desc.decode then return desc.decode(any_t.value or '') end + return require('pb.codec').decode(desc, any_t.value or '') +end + +-- --------------------------------------------------------------------------- +-- google.protobuf.FieldMask +-- message FieldMask { repeated string paths = 1; } +-- +-- Lua representation: Lua array of strings. (Plain table; no special tag.) +-- --------------------------------------------------------------------------- + +local function fieldmask_encode(v) + if v == nil or #v == 0 then return '' end + local out, n = {}, 0 + for i = 1, #v do + n = n + 1; out[n] = '\x0a' -- field 1, LEN + n = n + 1; out[n] = wire.encode_len(v[i]) + end + return table.concat(out) +end + +local function fieldmask_decode(buf) + local result = {} + local pos, len = 1, #buf + while pos <= len do + local id, wt + id, wt, pos = wire.decode_tag(buf, pos) + if id == 1 then + local s + s, pos = wire.decode_string(buf, pos) + result[#result + 1] = s + else + pos = wire.skip_field(buf, pos, wt) + end + end + return result +end + +M.FieldMask_encode = fieldmask_encode +M.FieldMask_decode = fieldmask_decode +M.FieldMask_descriptor = {name='google.protobuf.FieldMask', encode=fieldmask_encode, decode=fieldmask_decode} + +return M diff --git a/test/any_fieldmask_test.lua b/test/any_fieldmask_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..af0ad602eef6305ce04ca009c51b2615f01d67fd --- /dev/null +++ b/test/any_fieldmask_test.lua @@ -0,0 +1,169 @@ +-- Tests for google.protobuf.Any (with type registry) and FieldMask. +local t = require('luatest') +local pb = require('pb') +local wkt = pb.wkt + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +-- --------------------------------------------------------------------------- +-- FieldMask +-- --------------------------------------------------------------------------- +local gf = t.group('fieldmask.wire') + +gf.test_empty = function() + t.assert_equals(wkt.FieldMask_encode({}), '') + t.assert_equals(#wkt.FieldMask_decode(''), 0) +end + +gf.test_paths_round_trip = function() + local mask = {'user.email', 'user.address.city', 'enabled'} + local enc = wkt.FieldMask_encode(mask) + local dec = wkt.FieldMask_decode(enc) + t.assert_equals(#dec, 3) + t.assert_equals(dec[1], mask[1]) + t.assert_equals(dec[2], mask[2]) + t.assert_equals(dec[3], mask[3]) +end + +local gfj = t.group('fieldmask.json') +local hello = require('full.hello.hello_pb') + +local function reparse(s) return require('json').decode(s) end + +gfj.test_encode_canonical_camelcase = function() + local enc = pb.json.encode(hello.Event_descriptor, { + update_mask = {'user_id', 'is_admin', 'created_at'}, + }) + -- snake_case → lowerCamelCase, joined by commas. + t.assert_equals(reparse(enc).updateMask, 'userId,isAdmin,createdAt') +end + +gfj.test_decode_camelcase_to_snakecase = function() + local back = pb.json.decode(hello.Event_descriptor, + '{"updateMask": "userId,isAdmin"}') + t.assert_equals(back.update_mask[1], 'user_id') + t.assert_equals(back.update_mask[2], 'is_admin') +end + +-- --------------------------------------------------------------------------- +-- Any +-- --------------------------------------------------------------------------- +local ga = t.group('any.wire') + +ga.test_opaque_round_trip = function() + local opaque = { + type_url = 'type.googleapis.com/hello.Address', + value = '\x0a\x05hello', + } + local enc = wkt.Any_encode(opaque) + local dec = wkt.Any_decode(enc) + t.assert_equals(dec.type_url, opaque.type_url) + t.assert_equals(dec.value, opaque.value) +end + +ga.test_pack_unpack_via_registry = function() + pb.register(hello.Address_descriptor) + local boxed = pb.any.pack(hello.Address_descriptor, + {street = 'Main', city = 'X', zip = 1}) + t.assert_str_contains(boxed.type_url, 'hello.Address') + -- value is the wire-encoded Address payload. + local unpacked = pb.any.unpack(boxed) + t.assert_equals(unpacked.street, 'Main') + t.assert_equals(unpacked.city, 'X') + t.assert_equals(unpacked.zip, 1) +end + +ga.test_unpack_unregistered_errors = function() + t.assert_error_msg_contains( + 'no descriptor for', + function() pb.any.unpack({type_url = 'type.unknown/Foo', value = ''}) end) +end + +ga.test_unpack_with_explicit_descriptor_overrides_registry = function() + -- Even if not registered, an explicit descriptor lets unpack succeed. + local boxed = pb.any.pack(hello.Address_descriptor, {street = 'S'}) + local unpacked = pb.any.unpack(boxed, hello.Address_descriptor) + t.assert_equals(unpacked.street, 'S') +end + +local gaj = t.group('any.json') + +gaj.test_json_round_trip_with_registered_type = function() + pb.register(hello.Address_descriptor) + local e = { + extension = pb.any.pack(hello.Address_descriptor, + {street = 'JSON St', zip = 7}), + } + local enc = pb.json.encode(hello.Event_descriptor, e) + local obj = reparse(enc) + -- Flat form: @type plus the Address fields. + t.assert_equals(obj.extension['@type'], + 'type.googleapis.com/hello.Address') + t.assert_equals(obj.extension.street, 'JSON St') + t.assert_equals(obj.extension.zip, 7) + + -- Decode reverses. + local back = pb.json.decode(hello.Event_descriptor, enc) + t.assert_str_contains(back.extension.type_url, 'hello.Address') + local inner = pb.any.unpack(back.extension) + t.assert_equals(inner.street, 'JSON St') + t.assert_equals(inner.zip, 7) +end + +gaj.test_json_opaque_fallback_when_type_unregistered = function() + -- Untouched: even without registry knowledge the value field survives + -- through a base64 round-trip. + local opaque = {type_url = 'type.opaque/Foo', value = '\x01\x02\x03'} + local enc = pb.json.encode(hello.Event_descriptor, {extension = opaque}) + local obj = reparse(enc) + t.assert_equals(obj.extension['@type'], 'type.opaque/Foo') + t.assert(obj.extension.value, 'value field present as base64') + local back = pb.json.decode(hello.Event_descriptor, enc) + t.assert_equals(back.extension.type_url, 'type.opaque/Foo') + t.assert_equals(back.extension.value, '\x01\x02\x03') +end + +-- --------------------------------------------------------------------------- +-- End-to-end: Event message round-trip with extension + update_mask. +-- --------------------------------------------------------------------------- +for _, mode in ipairs({'full', 'runtime'}) do + local ge = t.group('any_fieldmask.event.' .. mode) + local h = require(mode .. '.hello.hello_pb') + + ge.test_event_with_any_and_mask = function() + pb.register(h.Address_descriptor) + local e = { + title = 'wrapped', + extension = pb.any.pack(h.Address_descriptor, + {street = 'A', city = 'B', zip = 5}), + update_mask = {'title', 'extension'}, + } + local dec = h.Event_decode(h.Event_encode(e)) + t.assert_equals(dec.title, 'wrapped') + t.assert_str_contains(dec.extension.type_url, 'hello.Address') + local inner = pb.any.unpack(dec.extension) + t.assert_equals(inner.street, 'A') + t.assert_equals(inner.zip, 5) + t.assert_equals(#dec.update_mask, 2) + t.assert_equals(dec.update_mask[1], 'title') + end +end + +local gp = t.group('any_fieldmask.parity') +local hello_full = require('full.hello.hello_pb') +local hello_runtime = require('runtime.hello.hello_pb') + +gp.test_bytes_match_across_modes = function() + pb.register(hello_full.Address_descriptor) + local e = { + extension = pb.any.pack(hello_full.Address_descriptor, + {street = 'Same', zip = 9}), + update_mask = {'a', 'b', 'c'}, + } + t.assert_equals(hex(hello_full.Event_encode(e)), + hex(hello_runtime.Event_encode(e))) +end diff --git a/test/conformance/proto/conformance.proto b/test/conformance/proto/conformance.proto new file mode 100644 index 0000000000000000000000000000000000000000..b4b2f315e0b436bd277a3b6f5877941ddc96f578 --- /dev/null +++ b/test/conformance/proto/conformance.proto @@ -0,0 +1,173 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +syntax = "proto3"; + +package conformance; + +option java_package = "com.google.protobuf.conformance"; +option objc_class_prefix = "Conformance"; + +// This defines the conformance testing protocol. This protocol exists between +// the conformance test suite itself and the code being tested. For each test, +// the suite will send a ConformanceRequest message and expect a +// ConformanceResponse message. +// +// You can either run the tests in two different ways: +// +// 1. in-process (using the interface in conformance_test.h). +// +// 2. as a sub-process communicating over a pipe. Information about how to +// do this is in conformance_test_runner.cc. +// +// Pros/cons of the two approaches: +// +// - running as a sub-process is much simpler for languages other than C/C++. +// +// - running as a sub-process may be more tricky in unusual environments like +// iOS apps, where fork/stdin/stdout are not available. + +enum WireFormat { + UNSPECIFIED = 0; + PROTOBUF = 1; + JSON = 2; + JSPB = 3; // Only used inside Google. Opensource testees just skip it. + TEXT_FORMAT = 4; +} + +enum TestCategory { + UNSPECIFIED_TEST = 0; + BINARY_TEST = 1; // Test binary wire format. + JSON_TEST = 2; // Test json wire format. + // Similar to JSON_TEST. However, during parsing json, testee should ignore + // unknown fields. This feature is optional. Each implementation can decide + // whether to support it. See + // https://developers.google.com/protocol-buffers/docs/proto3#json_options + // for more detail. + JSON_IGNORE_UNKNOWN_PARSING_TEST = 3; + // Test jspb wire format. Only used inside Google. Opensource testees just + // skip it. + JSPB_TEST = 4; + // Test text format. For cpp, java and python, testees can already deal with + // this type. Testees of other languages can simply skip it. + TEXT_FORMAT_TEST = 5; +} + +// Meant to encapsulate all types of tests: successes, skips, failures, etc. +// Therefore, this may or may not have a failure message. Failure messages +// may be truncated for our failure lists. +message TestStatus { + string name = 1; + string failure_message = 2; + // What an actual test name matched to in a failure list. Can be wildcarded or + // an exact match without wildcards. + string matched_name = 3; +} + +// The conformance runner will request a list of failures as the first request. +// This will be known by message_type == "conformance.FailureSet", a conformance +// test should return a serialized FailureSet in protobuf_payload. +message FailureSet { + repeated TestStatus test = 2; + reserved 1; +} + +// Represents a single test case's input. The testee should: +// +// 1. parse this proto (which should always succeed) +// 2. parse the protobuf or JSON payload in "payload" (which may fail) +// 3. if the parse succeeded, serialize the message in the requested format. +message ConformanceRequest { + // The payload (whether protobuf of JSON) is always for a + // protobuf_test_messages.proto3.TestAllTypes proto (as defined in + // src/google/protobuf/proto3_test_messages.proto). + oneof payload { + bytes protobuf_payload = 1; + string json_payload = 2; + // Only used inside Google. Opensource testees just skip it. + string jspb_payload = 7; + string text_payload = 8; + } + + // Which format should the testee serialize its message to? + WireFormat requested_output_format = 3; + + // The full name for the test message to use; for the moment, either: + // protobuf_test_messages.proto3.TestAllTypesProto3 or + // protobuf_test_messages.proto2.TestAllTypesProto2 or + // protobuf_test_messages.editions.proto2.TestAllTypesProto2 or + // protobuf_test_messages.editions.proto3.TestAllTypesProto3 or + // protobuf_test_messages.editions.TestAllTypesEdition2023 or + // protobuf_test_messages.edition_unstable.TestAllTypesEditionUnstable. + string message_type = 4; + + // Each test is given a specific test category. Some category may need + // specific support in testee programs. Refer to the definition of + // TestCategory for more information. + TestCategory test_category = 5; + + // Specify details for how to encode jspb. + JspbEncodingConfig jspb_encoding_options = 6; + + // This can be used in json and text format. If true, testee should print + // unknown fields instead of ignore. This feature is optional. + bool print_unknown_fields = 9; +} + +// Represents a single test case's output. +message ConformanceResponse { + oneof result { + // This string should be set to indicate parsing failed. The string can + // provide more information about the parse error if it is available. + // + // Setting this string does not necessarily mean the testee failed the + // test. Some of the test cases are intentionally invalid input. + string parse_error = 1; + + // If the input was successfully parsed but errors occurred when + // serializing it to the requested output format, set the error message in + // this field. + string serialize_error = 6; + + // This should be set if the test program timed out. The string should + // provide more information about what the child process was doing when it + // was killed. + string timeout_error = 9; + + // This should be set if some other error occurred. This will always + // indicate that the test failed. The string can provide more information + // about the failure. + string runtime_error = 2; + + // If the input was successfully parsed and the requested output was + // protobuf, serialize it to protobuf and set it in this field. + bytes protobuf_payload = 3; + + // If the input was successfully parsed and the requested output was JSON, + // serialize to JSON and set it in this field. + string json_payload = 4; + + // For when the testee skipped the test, likely because a certain feature + // wasn't supported, like JSON input/output. + string skipped = 5; + + // If the input was successfully parsed and the requested output was JSPB, + // serialize to JSPB and set it in this field. JSPB is only used inside + // Google. Opensource testees can just skip it. + string jspb_payload = 7; + + // If the input was successfully parsed and the requested output was + // TEXT_FORMAT, serialize to TEXT_FORMAT and set it in this field. + string text_payload = 8; + } +} + +// Encoding options for jspb format. +message JspbEncodingConfig { + // Encode the value field of Any as jspb array if true, otherwise binary. + bool use_jspb_array_any_format = 1; +} diff --git a/test/conformance/proto/test_messages_proto3.proto b/test/conformance/proto/test_messages_proto3.proto new file mode 100644 index 0000000000000000000000000000000000000000..8db90dab13b8863e76c41e45e6fa182949cf8c16 --- /dev/null +++ b/test/conformance/proto/test_messages_proto3.proto @@ -0,0 +1,272 @@ +// Protocol Buffers - Google's data interchange format +// Copyright 2008 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd +// +// Test schema for proto3 messages. This test schema is used by: +// +// - benchmarks +// - fuzz tests +// - conformance tests +// + +syntax = "proto3"; + +package protobuf_test_messages.proto3; + +option java_package = "com.google.protobuf_test_messages.proto3"; +option objc_class_prefix = "Proto3"; + +// This is the default, but we specify it here explicitly. +option optimize_for = SPEED; + +import "google/protobuf/any.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option cc_enable_arenas = true; + +// This proto includes every type of field in both singular and repeated +// forms. +// +// Also, crucially, all messages and enums in this file are eventually +// submessages of this message. So for example, a fuzz test of TestAllTypes +// could trigger bugs that occur in any message type in this file. We verify +// this stays true in a unit test. +message TestAllTypesProto3 { + message NestedMessage { + int32 a = 1; + TestAllTypesProto3 corecursive = 2; + } + + enum NestedEnum { + FOO = 0; + BAR = 1; + BAZ = 2; + NEG = -1; // Intentionally negative. + } + + enum AliasedEnum { + option allow_alias = true; + + ALIAS_FOO = 0; + ALIAS_BAR = 1; + ALIAS_BAZ = 2; + MOO = 2; + moo = 2; + bAz = 2; + } + + // Singular + // test [kotlin] comment + int32 optional_int32 = 1; + int64 optional_int64 = 2; + uint32 optional_uint32 = 3; + uint64 optional_uint64 = 4; + sint32 optional_sint32 = 5; + sint64 optional_sint64 = 6; + fixed32 optional_fixed32 = 7; + fixed64 optional_fixed64 = 8; + sfixed32 optional_sfixed32 = 9; + sfixed64 optional_sfixed64 = 10; + float optional_float = 11; + double optional_double = 12; + bool optional_bool = 13; + string optional_string = 14; + bytes optional_bytes = 15; + + NestedMessage optional_nested_message = 18; + ForeignMessage optional_foreign_message = 19; + + NestedEnum optional_nested_enum = 21; + ForeignEnum optional_foreign_enum = 22; + AliasedEnum optional_aliased_enum = 23; + + string optional_string_piece = 24 [ctype = STRING_PIECE]; + string optional_cord = 25 [ctype = CORD]; + + TestAllTypesProto3 recursive_message = 27; + + // Repeated + repeated int32 repeated_int32 = 31; + repeated int64 repeated_int64 = 32; + repeated uint32 repeated_uint32 = 33; + repeated uint64 repeated_uint64 = 34; + repeated sint32 repeated_sint32 = 35; + repeated sint64 repeated_sint64 = 36; + repeated fixed32 repeated_fixed32 = 37; + repeated fixed64 repeated_fixed64 = 38; + repeated sfixed32 repeated_sfixed32 = 39; + repeated sfixed64 repeated_sfixed64 = 40; + repeated float repeated_float = 41; + repeated double repeated_double = 42; + repeated bool repeated_bool = 43; + repeated string repeated_string = 44; + repeated bytes repeated_bytes = 45; + + repeated NestedMessage repeated_nested_message = 48; + repeated ForeignMessage repeated_foreign_message = 49; + + repeated NestedEnum repeated_nested_enum = 51; + repeated ForeignEnum repeated_foreign_enum = 52; + + repeated string repeated_string_piece = 54 [ctype = STRING_PIECE]; + repeated string repeated_cord = 55 [ctype = CORD]; + + // Packed + repeated int32 packed_int32 = 75 [packed = true]; + repeated int64 packed_int64 = 76 [packed = true]; + repeated uint32 packed_uint32 = 77 [packed = true]; + repeated uint64 packed_uint64 = 78 [packed = true]; + repeated sint32 packed_sint32 = 79 [packed = true]; + repeated sint64 packed_sint64 = 80 [packed = true]; + repeated fixed32 packed_fixed32 = 81 [packed = true]; + repeated fixed64 packed_fixed64 = 82 [packed = true]; + repeated sfixed32 packed_sfixed32 = 83 [packed = true]; + repeated sfixed64 packed_sfixed64 = 84 [packed = true]; + repeated float packed_float = 85 [packed = true]; + repeated double packed_double = 86 [packed = true]; + repeated bool packed_bool = 87 [packed = true]; + repeated NestedEnum packed_nested_enum = 88 [packed = true]; + + // Unpacked + repeated int32 unpacked_int32 = 89 [packed = false]; + repeated int64 unpacked_int64 = 90 [packed = false]; + repeated uint32 unpacked_uint32 = 91 [packed = false]; + repeated uint64 unpacked_uint64 = 92 [packed = false]; + repeated sint32 unpacked_sint32 = 93 [packed = false]; + repeated sint64 unpacked_sint64 = 94 [packed = false]; + repeated fixed32 unpacked_fixed32 = 95 [packed = false]; + repeated fixed64 unpacked_fixed64 = 96 [packed = false]; + repeated sfixed32 unpacked_sfixed32 = 97 [packed = false]; + repeated sfixed64 unpacked_sfixed64 = 98 [packed = false]; + repeated float unpacked_float = 99 [packed = false]; + repeated double unpacked_double = 100 [packed = false]; + repeated bool unpacked_bool = 101 [packed = false]; + repeated NestedEnum unpacked_nested_enum = 102 [packed = false]; + + // Map + map map_int32_int32 = 56; + map map_int64_int64 = 57; + map map_uint32_uint32 = 58; + map map_uint64_uint64 = 59; + map map_sint32_sint32 = 60; + map map_sint64_sint64 = 61; + map map_fixed32_fixed32 = 62; + map map_fixed64_fixed64 = 63; + map map_sfixed32_sfixed32 = 64; + map map_sfixed64_sfixed64 = 65; + map map_int32_float = 66; + map map_int32_double = 67; + map map_bool_bool = 68; + map map_string_string = 69; + map map_string_bytes = 70; + map map_string_nested_message = 71; + map map_string_foreign_message = 72; + map map_string_nested_enum = 73; + map map_string_foreign_enum = 74; + + oneof oneof_field { + uint32 oneof_uint32 = 111; + NestedMessage oneof_nested_message = 112; + string oneof_string = 113; + bytes oneof_bytes = 114; + bool oneof_bool = 115; + uint64 oneof_uint64 = 116; + float oneof_float = 117; + double oneof_double = 118; + NestedEnum oneof_enum = 119; + google.protobuf.NullValue oneof_null_value = 120; + } + + // Well-known types + google.protobuf.BoolValue optional_bool_wrapper = 201; + google.protobuf.Int32Value optional_int32_wrapper = 202; + google.protobuf.Int64Value optional_int64_wrapper = 203; + google.protobuf.UInt32Value optional_uint32_wrapper = 204; + google.protobuf.UInt64Value optional_uint64_wrapper = 205; + google.protobuf.FloatValue optional_float_wrapper = 206; + google.protobuf.DoubleValue optional_double_wrapper = 207; + google.protobuf.StringValue optional_string_wrapper = 208; + google.protobuf.BytesValue optional_bytes_wrapper = 209; + + repeated google.protobuf.BoolValue repeated_bool_wrapper = 211; + repeated google.protobuf.Int32Value repeated_int32_wrapper = 212; + repeated google.protobuf.Int64Value repeated_int64_wrapper = 213; + repeated google.protobuf.UInt32Value repeated_uint32_wrapper = 214; + repeated google.protobuf.UInt64Value repeated_uint64_wrapper = 215; + repeated google.protobuf.FloatValue repeated_float_wrapper = 216; + repeated google.protobuf.DoubleValue repeated_double_wrapper = 217; + repeated google.protobuf.StringValue repeated_string_wrapper = 218; + repeated google.protobuf.BytesValue repeated_bytes_wrapper = 219; + + google.protobuf.Duration optional_duration = 301; + google.protobuf.Timestamp optional_timestamp = 302; + google.protobuf.FieldMask optional_field_mask = 303; + google.protobuf.Struct optional_struct = 304; + google.protobuf.Any optional_any = 305; + google.protobuf.Value optional_value = 306; + google.protobuf.NullValue optional_null_value = 307; + google.protobuf.Empty optional_empty = 308; + + repeated google.protobuf.Duration repeated_duration = 311; + repeated google.protobuf.Timestamp repeated_timestamp = 312; + repeated google.protobuf.FieldMask repeated_fieldmask = 313; + repeated google.protobuf.Struct repeated_struct = 324; + repeated google.protobuf.Any repeated_any = 315; + repeated google.protobuf.Value repeated_value = 316; + repeated google.protobuf.ListValue repeated_list_value = 317; + repeated google.protobuf.Empty repeated_empty = 318; + + // Test field-name-to-JSON-name convention. + // (protobuf says names can be any valid C/C++ identifier.) + int32 fieldname1 = 401; + int32 field_name2 = 402; + int32 _field_name3 = 403; + int32 field__name4_ = 404; + int32 field0name5 = 405; + int32 field_0_name6 = 406; + int32 fieldName7 = 407; + int32 FieldName8 = 408; + int32 field_Name9 = 409; + int32 Field_Name10 = 410; + int32 FIELD_NAME11 = 411; + int32 FIELD_name12 = 412; + int32 __field_name13 = 413; + int32 __Field_name14 = 414; + int32 field__name15 = 415; + int32 field__Name16 = 416; + int32 field_name17__ = 417; + int32 Field_name18__ = 418; + + // Reserved for testing unknown fields + reserved 501 to 510; + + reserved "reserved_field"; + reserved 999999; +} + +message ForeignMessage { + int32 c = 1; +} + +enum ForeignEnum { + FOREIGN_FOO = 0; + FOREIGN_BAR = 1; + FOREIGN_BAZ = 2; +} + +message NullHypothesisProto3 {} + +message EnumOnlyProto3 { + enum Bool { + kFalse = 0; + kTrue = 1; + } +} diff --git a/test/conformance_test.lua b/test/conformance_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..f4af69572b65d05bd8e5a39af9efad88d91623d9 --- /dev/null +++ b/test/conformance_test.lua @@ -0,0 +1,282 @@ +-- Self-test for the conformance runner. +-- +-- The Google conformance suite is an external binary +-- (`conformance_test_runner`) we can't reasonably bundle here, so this +-- test stands in for it: drives our runner with crafted +-- `ConformanceRequest` cases and asserts well-formed +-- `ConformanceResponse` output. +-- +-- Two layers: +-- 1. `core` group — calls `cmd.conformance.core.handle_request` directly. +-- Covers every dispatch arm without paying subprocess cost. +-- 2. `subprocess` group — actually pipes framed bytes through +-- `tarantool cmd/conformance-runner.lua`. Covers the length-prefixed +-- framing and the read-until-EOF loop. + +local t = require('luatest') +local fio = require('fio') +local core = require('cmd.conformance.core') +local conformance = require('full.conformance.conformance_pb') +local proto3 = require('full.protobuf_test_messages.proto3.test_messages_proto3_pb') + +local PROTOBUF = conformance.WireFormat.PROTOBUF +local JSON = conformance.WireFormat.JSON +local TEXT = conformance.WireFormat.TEXT_FORMAT + +local PROTO3_NAME = 'protobuf_test_messages.proto3.TestAllTypesProto3' + +local function encode_req(t_) + return conformance.ConformanceRequest_encode(t_) +end + +local function decode_resp(bytes) + return conformance.ConformanceResponse_decode(bytes) +end + +-- --------------------------------------------------------------------------- +-- 1. Direct dispatch (no subprocess) +-- --------------------------------------------------------------------------- + +local core_g = t.group('conformance.core') + +core_g.test_failureset_preflight = function() + -- The conformance runner asks for a FailureSet up front. Empty payload, + -- empty FailureSet response is the canonical answer. + local req = encode_req({ + protobuf_payload = '', + requested_output_format = PROTOBUF, + message_type = 'conformance.FailureSet', + }) + local resp = decode_resp(core.handle_request(req)) + t.assert_equals(resp.protobuf_payload, '') + t.assert_equals(resp.skipped, nil) + t.assert_equals(resp.parse_error, nil) + t.assert_equals(resp.runtime_error, nil) +end + +core_g.test_pb_to_pb_roundtrip = function() + -- Encode a TestAllTypesProto3 ourselves, ask the runner to round-trip + -- it through pb->pb, assert byte-identical output. Since our encoder + -- is deterministic for non-map fields, the output bytes must match + -- input bytes exactly. + local input = proto3.TestAllTypesProto3_encode({ + optional_int32 = 42, + optional_string = 'hello', + repeated_int32 = {1, 2, 3, 4}, + }) + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = input, + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_equals(resp.protobuf_payload, input) +end + +core_g.test_pb_to_json = function() + local input = proto3.TestAllTypesProto3_encode({ + optional_int32 = 7, + optional_string = 'world', + }) + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = input, + requested_output_format = JSON, + message_type = PROTO3_NAME, + }))) + t.assert_str_contains(resp.json_payload, '"optionalInt32":7') + t.assert_str_contains(resp.json_payload, '"optionalString":"world"') +end + +core_g.test_json_to_pb = function() + local resp = decode_resp(core.handle_request(encode_req({ + json_payload = [[{"optionalInt32": 9, "optionalString": "abc"}]], + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_not(resp.parse_error, resp.parse_error) + t.assert_not(resp.runtime_error, resp.runtime_error) + local decoded = proto3.TestAllTypesProto3_decode(resp.protobuf_payload) + t.assert_equals(decoded.optional_int32, 9) + t.assert_equals(decoded.optional_string, 'abc') +end + +core_g.test_parse_error_on_malformed_protobuf = function() + -- A truncated varint should produce a parse_error, not a crash. + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = '\x08', -- tag(1, VARINT), no value + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_not_equals(resp.parse_error, nil) +end + +core_g.test_parse_error_on_malformed_json = function() + local resp = decode_resp(core.handle_request(encode_req({ + json_payload = '{not valid json', + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_not_equals(resp.parse_error, nil) +end + +core_g.test_unsupported_message_type_skipped = function() + -- proto2 / editions test message types are intentionally unsupported. + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = '', + requested_output_format = PROTOBUF, + message_type = 'protobuf_test_messages.proto2.TestAllTypesProto2', + }))) + t.assert_str_contains(resp.skipped or '', 'unsupported message type') +end + +core_g.test_text_format_skipped = function() + local input = proto3.TestAllTypesProto3_encode({optional_int32 = 1}) + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = input, + requested_output_format = TEXT, + message_type = PROTO3_NAME, + }))) + t.assert_str_contains(resp.skipped or '', 'jspb/text') +end + +core_g.test_empty_payload_decodes_as_empty_message = function() + -- proto3 says empty bytes is a valid empty message. + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = '', + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_equals(resp.protobuf_payload, '') +end + +core_g.test_wkt_timestamp_field_roundtrip = function() + -- Exercise the WKT path: TestAllTypesProto3.optional_timestamp. + -- We rely on our WKT Timestamp encoder/decoder. + local input = proto3.TestAllTypesProto3_encode({ + optional_timestamp = require('datetime').new({timestamp = 1700000000}), + }) + local resp = decode_resp(core.handle_request(encode_req({ + protobuf_payload = input, + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }))) + t.assert_equals(resp.protobuf_payload, input) +end + +-- --------------------------------------------------------------------------- +-- 2. Subprocess: stdin/stdout framing +-- --------------------------------------------------------------------------- +-- +-- These tests verify only the framing wrapper in cmd/conformance-runner.lua; +-- the dispatch logic is fully covered by the `core` group above. + +local sub_g = t.group('conformance.subprocess') + +local REPO_ROOT = fio.abspath(fio.pathjoin( + fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..')) +local RUNNER = fio.pathjoin(REPO_ROOT, 'cmd', 'conformance-runner.lua') + +local function le32(n) + return string.char( + n % 256, + math.floor(n / 256) % 256, + math.floor(n / 65536) % 256, + math.floor(n / 16777216) % 256) +end + +local function read_le32(s, off) + local b1, b2, b3, b4 = s:byte(off, off + 3) + return b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 +end + +-- Run the runner with `input_bytes` piped to stdin, return the raw stdout. +local function run_runner(input_bytes) + local in_path = os.tmpname() + local out_path = os.tmpname() + local err_path = os.tmpname() + local fin = assert(io.open(in_path, 'wb')) + fin:write(input_bytes); fin:close() + + local lua_path = os.getenv('LUA_PATH') or '' + local cmd = string.format( + 'cd %q && LUA_PATH=%q tarantool cmd/conformance-runner.lua < %q > %q 2> %q', + REPO_ROOT, lua_path, in_path, out_path, err_path) + local rc = os.execute(cmd) + + local fout = assert(io.open(out_path, 'rb')) + local out = fout:read('*a'); fout:close() + local ferr = io.open(err_path, 'r') + local err = ferr and ferr:read('*a') or '' + if ferr then ferr:close() end + os.remove(in_path); os.remove(out_path); os.remove(err_path) + return rc, out, err +end + +-- Parse a stream of length-prefixed responses out of `bytes`. +local function parse_framed(bytes) + local out, off = {}, 1 + while off + 4 <= #bytes + 1 do + local n = read_le32(bytes, off) + off = off + 4 + if off + n - 1 > #bytes then break end + out[#out + 1] = bytes:sub(off, off + n - 1) + off = off + n + end + return out, off +end + +sub_g.test_single_request_round_trip = function() + local req = encode_req({ + protobuf_payload = '', + requested_output_format = PROTOBUF, + message_type = 'conformance.FailureSet', + }) + local framed = le32(#req) .. req + local rc, out, err = run_runner(framed) + t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) + local resps = parse_framed(out) + t.assert_equals(#resps, 1, 'expected 1 framed response, stderr=' .. err) + local resp = decode_resp(resps[1]) + t.assert_equals(resp.protobuf_payload, '') +end + +sub_g.test_multiple_requests_in_one_session = function() + -- The conformance runner sends many requests over a single pipe; the + -- script must loop until EOF rather than handle one and exit. + local req1 = encode_req({ + protobuf_payload = '', + requested_output_format = PROTOBUF, + message_type = 'conformance.FailureSet', + }) + local payload = proto3.TestAllTypesProto3_encode({optional_int32 = 17}) + local req2 = encode_req({ + protobuf_payload = payload, + requested_output_format = PROTOBUF, + message_type = PROTO3_NAME, + }) + local req3 = encode_req({ + json_payload = [[{"optionalInt32": 99}]], + requested_output_format = JSON, + message_type = PROTO3_NAME, + }) + + local framed = le32(#req1) .. req1 + .. le32(#req2) .. req2 + .. le32(#req3) .. req3 + local rc, out, err = run_runner(framed) + t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) + local resps = parse_framed(out) + t.assert_equals(#resps, 3, 'expected 3 framed responses, stderr=' .. err) + + local r1 = decode_resp(resps[1]) + local r2 = decode_resp(resps[2]) + local r3 = decode_resp(resps[3]) + t.assert_equals(r1.protobuf_payload, '') + t.assert_equals(r2.protobuf_payload, payload) + t.assert_str_contains(r3.json_payload, '"optionalInt32":99') +end + +sub_g.test_empty_stdin_clean_exit = function() + local rc, out, err = run_runner('') + t.assert_equals(rc, 0, 'runner exited non-zero, stderr=' .. err) + t.assert_equals(out, '', 'unexpected output on empty stdin') +end diff --git a/test/dynamic_test.lua b/test/dynamic_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..3c63644e46844221f153d7b7269eb29a4f9bdfa1 --- /dev/null +++ b/test/dynamic_test.lua @@ -0,0 +1,135 @@ +-- Runtime parsing: load hello.proto at runtime and assert encode/decode +-- parity with the build-time generated module across the interop corpus. +local t = require('luatest') +local fio = require('fio') +local pb = require('pb') + +local REPO_ROOT = fio.abspath(fio.pathjoin( + fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..')) +local PROTO_PATH = fio.pathjoin(REPO_ROOT, 'examples', 'proto', 'hello.proto') +local FIXTURES_DIR = fio.pathjoin(REPO_ROOT, 'test', 'interop', 'fixtures') + +local function slurp(path) + local f = assert(io.open(path, 'rb')) + local s = f:read('*a') + f:close() + return s +end + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +local g_unit = t.group('parser.unit') + +g_unit.test_parser_handles_full_schema = function() + local source = slurp(PROTO_PATH) + local ast = pb.parser.parse(source) + t.assert_equals(ast.syntax, 'proto3') + t.assert_equals(ast.package, 'hello') + t.assert(#ast.messages > 0) + -- Look up Person AST in flat list. + local person + for _, m in ipairs(ast.messages) do + if m.name == 'Person' then person = m; break end + end + t.assert(person, 'Person message found') + -- Locate `optional string apartment = 4;` on Address + local address + for _, m in ipairs(ast.messages) do + if m.name == 'Address' then address = m; break end + end + t.assert(address) + local apt + for _, f in ipairs(address.fields) do + if f.name == 'apartment' then apt = f; break end + end + t.assert(apt and apt.optional, 'apartment is optional') +end + +g_unit.test_parser_handles_oneof_and_map = function() + local source = slurp(PROTO_PATH) + local ast = pb.parser.parse(source) + -- Find Result; should have a oneof. + local result + for _, m in ipairs(ast.messages) do + if m.name == 'Result' then result = m; break end + end + t.assert(result and #result.oneofs > 0) + t.assert_equals(result.oneofs[1].name, 'outcome') + -- Find Person; should have map fields. + local person + for _, m in ipairs(ast.messages) do + if m.name == 'Person' then person = m; break end + end + local map_field + for _, f in ipairs(person.fields) do + if f.kind == 'map' then map_field = f; break end + end + t.assert(map_field, 'at least one map field') +end + +-- --------------------------------------------------------------------------- +-- Interop: dynamic module must produce identical bytes to the generated +-- module for every fixture in the corpus. +-- --------------------------------------------------------------------------- + +local hello_dynamic = pb.parse(slurp(PROTO_PATH)) +local hello_static = require('full.hello.hello_pb') + +local function fixtures() + local entries = fio.listdir(FIXTURES_DIR) + table.sort(entries) + local out = {} + for _, name in ipairs(entries) do + if name:match('%.bin$') then + local base = name:sub(1, -5) + local bin = fio.pathjoin(FIXTURES_DIR, name) + local txt = fio.pathjoin(FIXTURES_DIR, base .. '.txtpb') + local type_full + for line in io.lines(txt) do + local m = line:match('^# type:%s*(%S+)') + if m then type_full = m; break end + end + out[#out + 1] = {base = base, bin = bin, full = type_full} + end + end + return out +end + +local g = t.group('parser.interop') +for _, fx in ipairs(fixtures()) do + g['test_' .. fx.base] = function() + local short = fx.full:gsub('^hello%.', '') + local enc = hello_dynamic[short .. '_encode'] + local dec = hello_dynamic[short .. '_decode'] + t.assert(enc and dec, 'dynamic module has ' .. short) + + local golden = slurp(fx.bin) + local decoded = dec(golden) + local reencoded = enc(decoded) + + t.assert_equals(hex(reencoded), hex(golden), + 'dynamic round-trip diverges from golden for ' .. fx.base) + end +end + +-- --------------------------------------------------------------------------- +-- Cross-module: dynamic-decoded message can be re-encoded via the +-- generated module byte-for-byte, and vice versa. +-- --------------------------------------------------------------------------- +local g_cross = t.group('parser.cross_module') +g_cross.test_cross_module_byte_equality = function() + for _, fx in ipairs(fixtures()) do + local short = fx.full:gsub('^hello%.', '') + local golden = slurp(fx.bin) + local from_dyn = hello_dynamic[short .. '_decode'](golden) + local from_stat = hello_static[short .. '_decode'](golden) + t.assert_equals( + hex(hello_static[short .. '_encode'](from_dyn)), + hex(hello_dynamic[short .. '_encode'](from_stat)), + 'cross-module mismatch on ' .. fx.base) + end +end diff --git a/test/grpc_streaming_test.lua b/test/grpc_streaming_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..6ec34869645624e121bf7fb4a7862d87b6fd67b7 --- /dev/null +++ b/test/grpc_streaming_test.lua @@ -0,0 +1,302 @@ +-- gRPC streaming over fiber channels: server-streaming, client-streaming, +-- and bidirectional flavors, run end-to-end through the loopback transport. +-- +-- Parameterized over both codegen modes since the generated client/server +-- bodies differ between full and runtime mode (different per-message +-- encode/decode call sites). + +local t = require('luatest') +local fiber = require('fiber') +local pb = require('pb') + +local MODES = {'full', 'runtime'} + +for _, mode in ipairs(MODES) do + local hello = require(mode .. '.hello.hello_pb') + local g = t.group('grpc_stream.' .. mode) + + -- --------------------------------------------------------------------- + -- Server-streaming: one request in, many replies out. + -- --------------------------------------------------------------------- + + g.test_server_stream_yields_replies_in_order = function() + local impl = { + StreamHellos = function(req, stream, _) + for i = 1, 3 do + stream:send({greeting = req.name .. '#' .. tostring(i)}) + end + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + + local stream = client.StreamHellos({name = 'X'}, {}) + local replies = {} + while true do + local r, err = stream:recv() + if r == nil then + t.assert_equals(err, nil, 'unexpected stream error: ' .. tostring(err)) + break + end + replies[#replies + 1] = r.greeting + end + t.assert_equals(replies, {'X#1', 'X#2', 'X#3'}) + end + + g.test_server_stream_handler_can_be_empty = function() + -- A handler that returns without sending anything is a valid + -- (empty) stream — recv() must return nil on the first call. + local impl = {StreamHellos = function(_, _, _) end} + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + local stream = client.StreamHellos({name = 'x'}, {}) + local r, err = stream:recv() + t.assert_equals(r, nil) + t.assert_equals(err, nil) + end + + g.test_server_stream_handler_error_surfaces = function() + local impl = { + StreamHellos = function(_, stream, _) + stream:send({greeting = 'first'}) + error('boom from handler') + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + local stream = client.StreamHellos({name = 'x'}, {}) + + -- First message comes through cleanly. + local r1 = stream:recv() + t.assert_equals(r1.greeting, 'first') + + -- Then end-of-stream with the handler's error string. + local r2, err = stream:recv() + t.assert_equals(r2, nil) + t.assert_str_contains(tostring(err), 'boom from handler') + end + + g.test_missing_streaming_handler_errors = function() + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server({}))) + local stream = client.StreamHellos({name = 'x'}, {}) + local r, err = stream:recv() + t.assert_equals(r, nil) + t.assert_str_contains(tostring(err), 'StreamHellos: handler missing') + end + + -- --------------------------------------------------------------------- + -- Client-streaming: many requests in, one reply out. + -- --------------------------------------------------------------------- + + g.test_client_stream_collects_and_replies = function() + local impl = { + CollectHellos = function(stream, _) + local names = {} + while true do + local req = stream:recv() + if req == nil then break end + names[#names + 1] = req.name + end + return {greeting = 'collected: ' .. table.concat(names, ',')} + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + + local call = client.CollectHellos({}) + call:send({name = 'a'}) + call:send({name = 'b'}) + call:send({name = 'c'}) + call:close_send() + + local reply, err = call:recv() + t.assert_equals(err, nil) + t.assert_equals(reply.greeting, 'collected: a,b,c') + + -- recv after the response should return nil (stream ended). + local tail, err2 = call:recv() + t.assert_equals(tail, nil) + t.assert_equals(err2, nil) + end + + g.test_client_stream_empty_is_valid = function() + local impl = { + CollectHellos = function(stream, _) + t.assert_equals(stream:recv(), nil) + return {greeting = 'empty'} + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + local call = client.CollectHellos({}) + call:close_send() + local reply = call:recv() + t.assert_equals(reply.greeting, 'empty') + end + + -- --------------------------------------------------------------------- + -- Bidirectional streaming: both sides send + recv independently. + -- --------------------------------------------------------------------- + + g.test_bidi_echo_loop = function() + -- Server echoes each request back with a "you said: " prefix. + local impl = { + Chat = function(stream, _) + while true do + local req = stream:recv() + if req == nil then break end + stream:send({greeting = 'you said: ' .. req.name}) + end + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + + local call = client.Chat({}) + + -- Drive the client side from a separate fiber so we can interleave + -- sends and receives without deadlocking. + local reader_done = fiber.channel(1) + local received = {} + fiber.create(function() + while true do + local r, err = call:recv() + if r == nil then + reader_done:put(err or false) + break + end + received[#received + 1] = r.greeting + end + end) + + call:send({name = 'foo'}) + call:send({name = 'bar'}) + call:send({name = 'baz'}) + call:close_send() + + local terminal = reader_done:get(5) + t.assert(terminal == false, 'reader saw error: ' .. tostring(terminal)) + t.assert_equals(received, { + 'you said: foo', 'you said: bar', 'you said: baz', + }) + end + + g.test_bidi_handler_error_surfaces = function() + local impl = { + Chat = function(_, _) + error('chat exploded') + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + local call = client.Chat({}) + call:close_send() + local r, err = call:recv() + t.assert_equals(r, nil) + t.assert_str_contains(tostring(err), 'chat exploded') + end + + g.test_bidi_cancel_stops_further_recv = function() + -- Server sends forever; client cancels after one message. + local server_finished = fiber.channel(1) + local impl = { + Chat = function(stream, _) + local i = 0 + while stream:send({greeting = 'tick:' .. tostring(i)}) do + i = i + 1 + if i > 100 then break end -- safety + fiber.yield() + end + server_finished:put(i) + end, + } + local client = hello.Greeter_client(pb.grpc.loopback( + hello.Greeter_server(impl))) + local call = client.Chat({}) + local first = call:recv() + t.assert_str_contains(first.greeting, 'tick:') + call:cancel() + + -- Give the server fiber a chance to observe the cancel. + local final_i = server_finished:get(5) + t.assert(final_i ~= nil, 'server did not stop after cancel') + t.assert(final_i <= 100, 'server kept running past safety bound') + end +end + +-- --------------------------------------------------------------------------- +-- pb.grpc.new_stream_pair: unit-level coverage of the primitive itself +-- (independent of generated code). +-- --------------------------------------------------------------------------- + +local u = t.group('grpc_stream.primitive') + +u.test_send_recv_byte_passthrough = function() + local client, server = pb.grpc.new_stream_pair() + fiber.create(function() + local req = server:recv() + server:send('reply:' .. req) + server:_finish(nil) + end) + client:send('ping') + client:close_send() + local out = client:recv() + t.assert_equals(out, 'reply:ping') + t.assert_equals(client:recv(), nil) +end + +u.test_finish_with_error_propagates = function() + local client, server = pb.grpc.new_stream_pair() + fiber.create(function() + server:_finish('handler error') + end) + local b, err = client:recv() + t.assert_equals(b, nil) + t.assert_equals(err, 'handler error') +end + +u.test_send_after_close_send_errors = function() + local client, _ = pb.grpc.new_stream_pair() + client:close_send() + t.assert_error(function() client:send('nope') end) +end + +-- --------------------------------------------------------------------------- +-- multiplex: streaming methods route correctly across multiple servers. +-- --------------------------------------------------------------------------- + +local m = t.group('grpc_stream.multiplex') + +m.test_multiplex_routes_streams_to_right_server = function() + local hello = require('full.hello.hello_pb') + -- Two independent Greeter servers; multiplex would normally complain + -- about the duplicate paths, so for this test we keep just one with a + -- streaming impl and verify routing works via the multiplex transport. + local impl = { + StreamHellos = function(req, stream, _) + stream:send({greeting = 'mp:' .. req.name}) + end, + } + local mux = pb.grpc.multiplex({hello.Greeter_server(impl)}) + local client = hello.Greeter_client(mux) + local s = client.StreamHellos({name = 'X'}, {}) + local r = s:recv() + t.assert_equals(r.greeting, 'mp:X') + t.assert_equals(s:recv(), nil) +end + +m.test_multiplex_duplicate_streaming_path_errors = function() + -- Construct two minimal server tables that share a streaming path so + -- we hit the duplicate-stream branch without also tripping the + -- duplicate-unary branch. + local stream_entry = { + kind = 'server_stream', + handler = function() end, + } + local s1 = {methods = {}, streams = {['/dup/Path'] = stream_entry}} + local s2 = {methods = {}, streams = {['/dup/Path'] = stream_entry}} + t.assert_error_msg_contains('duplicate streaming route', function() + pb.grpc.multiplex({s1, s2}) + end) +end diff --git a/test/interop/fixtures/address_basic.bin b/test/interop/fixtures/address_basic.bin new file mode 100644 index 0000000000000000000000000000000000000000..3be38de07d9298d88c9df466b4759409fba7f129 --- /dev/null +++ b/test/interop/fixtures/address_basic.bin @@ -0,0 +1,3 @@ + + +Pushkina 1Moscow \ No newline at end of file diff --git a/test/interop/fixtures/address_basic.txtpb b/test/interop/fixtures/address_basic.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..105e70cf77e5cca5ec963a9c6d7981bf42bcd01b --- /dev/null +++ b/test/interop/fixtures/address_basic.txtpb @@ -0,0 +1,4 @@ +# type: hello.Address +street: "Pushkina 1" +city: "Moscow" +zip: 123456 diff --git a/test/interop/fixtures/address_with_optional.bin b/test/interop/fixtures/address_with_optional.bin new file mode 100644 index 0000000000000000000000000000000000000000..03f696a2734f133dfb104232bdd323244c40eab9 GIT binary patch literal 10 Rcmd;L@lDLklVDV0000W)0nY#c literal 0 HcmV?d00001 diff --git a/test/interop/fixtures/address_with_optional.txtpb b/test/interop/fixtures/address_with_optional.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..d01ee5d9711c1b621d47d27dc9838ca885794b8a --- /dev/null +++ b/test/interop/fixtures/address_with_optional.txtpb @@ -0,0 +1,4 @@ +# type: hello.Address +street: "Main" +zip: 1 +apartment: "" diff --git a/test/interop/fixtures/event_any_fieldmask.bin b/test/interop/fixtures/event_any_fieldmask.bin new file mode 100644 index 0000000000000000000000000000000000000000..71de5d49993fe65af8d18b25c434176014619ea4 --- /dev/null +++ b/test/interop/fixtures/event_any_fieldmask.bin @@ -0,0 +1,7 @@ + +wrapZ0 +!type.googleapis.com/hello.Address +4MainXb +title + extension +tags \ No newline at end of file diff --git a/test/interop/fixtures/event_any_fieldmask.txtpb b/test/interop/fixtures/event_any_fieldmask.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..292932e75697de43477d0b3506d8dc8c3a752e84 --- /dev/null +++ b/test/interop/fixtures/event_any_fieldmask.txtpb @@ -0,0 +1,11 @@ +# type: hello.Event +title: "wrap" +extension { + type_url: "type.googleapis.com/hello.Address" + value: "\n4Main\022\001X\030\001" +} +update_mask { + paths: "title" + paths: "extension" + paths: "tags" +} diff --git a/test/interop/fixtures/event_struct_value.bin b/test/interop/fixtures/event_struct_value.bin new file mode 100644 index 0000000000000000000000000000000000000000..8597bb48351c60762c8f0413983f0af3ca47b577 GIT binary patch literal 68 zcmd;L&&(@HEy^!&66X@-Vk=5b&&%E%0o;^Gu!0D}+q TT&z;eCFS{CObU!#OdJdVKzj~A literal 0 HcmV?d00001 diff --git a/test/interop/fixtures/event_struct_value.txtpb b/test/interop/fixtures/event_struct_value.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..a673ecbcbdb8c5cbb7d1b103af48ea2a7a64fd31 --- /dev/null +++ b/test/interop/fixtures/event_struct_value.txtpb @@ -0,0 +1,15 @@ +# type: hello.Event +# Single-key payload to avoid map-iteration order ambiguity. The multi-key +# Struct cases are covered by struct_value_test.lua round-trip assertions; +# this fixture pins our wire bytes to mainline protoc's output. +title: "interop" +payload { + fields { key: "region" value { string_value: "us-east-1" } } +} +attribute { string_value: "hi" } +tags { + values { number_value: 1 } + values { string_value: "two" } + values { bool_value: true } + values { null_value: NULL_VALUE } +} diff --git a/test/interop/fixtures/person_map.bin b/test/interop/fixtures/person_map.bin new file mode 100644 index 0000000000000000000000000000000000000000..189feab0514d5ac58876c42567ff05d48be14633 --- /dev/null +++ b/test/interop/fixtures/person_map.bin @@ -0,0 +1,6 @@ + + MapHolderj +alicej +bobz +home +MainX \ No newline at end of file diff --git a/test/interop/fixtures/person_map.txtpb b/test/interop/fixtures/person_map.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..39c0cb232d62e8ecb85d0493451d89c63088767f --- /dev/null +++ b/test/interop/fixtures/person_map.txtpb @@ -0,0 +1,5 @@ +# type: hello.Person +name: "MapHolder" +ages_by_nickname { key: "alice" value: 30 } +ages_by_nickname { key: "bob" value: 25 } +addresses_by_label { key: "home" value: { street: "Main" city: "X" zip: 1 } } diff --git a/test/interop/fixtures/person_nested.bin b/test/interop/fixtures/person_nested.bin new file mode 100644 index 0000000000000000000000000000000000000000..8128bf89e744a44810226590a1b4ea5011da1472 --- /dev/null +++ b/test/interop/fixtures/person_nested.bin @@ -0,0 +1,6 @@ + +Root* +Main St Springfieldd2 +Alice2 +Bob2 +Carol \ No newline at end of file diff --git a/test/interop/fixtures/person_nested.txtpb b/test/interop/fixtures/person_nested.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..a09a815a711404afb3a931cebfb213429bcb5032 --- /dev/null +++ b/test/interop/fixtures/person_nested.txtpb @@ -0,0 +1,18 @@ +# type: hello.Person +name: "Root" +friends { + name: "Alice" + age: 25 +} +friends { + name: "Bob" + age: 31 + friends { + name: "Carol" + } +} +address { + street: "Main St" + city: "Springfield" + zip: 100 +} diff --git a/test/interop/fixtures/person_packed.bin b/test/interop/fixtures/person_packed.bin new file mode 100644 index 0000000000000000000000000000000000000000..b1b8fccea6a7232c1e3cb44d82e867571bc2bc70 --- /dev/null +++ b/test/interop/fixtures/person_packed.bin @@ -0,0 +1,2 @@ + +Alice a@example.com b@example.com : \ No newline at end of file diff --git a/test/interop/fixtures/person_packed.txtpb b/test/interop/fixtures/person_packed.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..bc8c38b09111bfbe9e0617409415f430e480b15b --- /dev/null +++ b/test/interop/fixtures/person_packed.txtpb @@ -0,0 +1,11 @@ +# type: hello.Person +name: "Alice" +age: 30 +emails: "a@example.com" +emails: "b@example.com" +status: OK +lucky_numbers: 1 +lucky_numbers: 2 +lucky_numbers: 3 +lucky_numbers: -7 +lucky_numbers: 1024 diff --git a/test/interop/fixtures/person_wires.bin b/test/interop/fixtures/person_wires.bin new file mode 100644 index 0000000000000000000000000000000000000000..97b1bad47192714411e1a3cee41e105273cefaf2 GIT binary patch literal 33 mcmd;Lj8I^5Vqsup`tQluP~xzH@BP1kj|Uhd89+cG$N>P5atQnY literal 0 HcmV?d00001 diff --git a/test/interop/fixtures/person_wires.txtpb b/test/interop/fixtures/person_wires.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..d21500ff21b1649b957694437a31c4517eb9353d --- /dev/null +++ b/test/interop/fixtures/person_wires.txtpb @@ -0,0 +1,7 @@ +# type: hello.Person +name: "X" +avatar: "\000\001\002\377" +user_id: 18369917520866213889 +balance: -12345 +weight_kg: 72.5 +status: ERROR diff --git a/test/interop/fixtures/result_oneof_msg.bin b/test/interop/fixtures/result_oneof_msg.bin new file mode 100644 index 0000000000000000000000000000000000000000..a4e759cf31af4289c9dd6e3d118d05f19e68d108 --- /dev/null +++ b/test/interop/fixtures/result_oneof_msg.bin @@ -0,0 +1,2 @@ +" +Xc \ No newline at end of file diff --git a/test/interop/fixtures/result_oneof_msg.txtpb b/test/interop/fixtures/result_oneof_msg.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..fe48c994a41734de692399a04ed6f13dab7b4cca --- /dev/null +++ b/test/interop/fixtures/result_oneof_msg.txtpb @@ -0,0 +1,3 @@ +# type: hello.Result +id: 3 +details { street: "X" zip: 99 } diff --git a/test/interop/fixtures/result_oneof_text.bin b/test/interop/fixtures/result_oneof_text.bin new file mode 100644 index 0000000000000000000000000000000000000000..b3a11b396574b0915f8fdf953d178bfe37de4678 --- /dev/null +++ b/test/interop/fixtures/result_oneof_text.bin @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/test/interop/fixtures/result_oneof_text.txtpb b/test/interop/fixtures/result_oneof_text.txtpb new file mode 100644 index 0000000000000000000000000000000000000000..e0d04e58cbf9810ea205e01a8e9ebf073d15cdfa --- /dev/null +++ b/test/interop/fixtures/result_oneof_text.txtpb @@ -0,0 +1,3 @@ +# type: hello.Result +id: 7 +text: "hello" diff --git a/test/interop_test.lua b/test/interop_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..b01b17280cb38f5a1cc494cd166f6008401997c2 --- /dev/null +++ b/test/interop_test.lua @@ -0,0 +1,88 @@ +-- Cross-implementation interop: assert our codec is byte-for-byte equivalent +-- to mainline protoc. +-- +-- For each fixture .bin (the output of `protoc --encode=Type` against +-- the matching .txtpb), this suite: +-- (a) decodes the golden bytes with our decoder +-- (b) re-encodes the resulting Lua table with our encoder +-- (c) asserts the round-tripped bytes match the golden byte-for-byte +-- +-- Step (c) is the strong claim: we don't just round-trip ourselves, we +-- agree on the wire with the canonical implementation. +local t = require('luatest') +local fio = require('fio') + +local FIXTURES_DIR = fio.abspath(fio.pathjoin( + fio.dirname(debug.getinfo(1, 'S').source:sub(2)), 'interop', 'fixtures')) + +local function slurp(path) + local f = assert(io.open(path, 'rb')) + local s = f:read('*a') + f:close() + return s +end + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +-- Read the `# type: ` annotation from the matching .txtpb file +-- to learn which Lua message type to decode/encode with. +local function read_type(txtpb_path) + for line in io.lines(txtpb_path) do + local m = line:match('^# type:%s*(%S+)') + if m then return m end + end + error('no `# type:` annotation in ' .. txtpb_path, 0) +end + +local function lua_type_funcs(hello, full_name) + -- "hello.Person" -> ("Person_encode", "Person_decode") + local short = full_name:gsub('^hello%.', '') + return hello[short .. '_encode'], hello[short .. '_decode'] +end + +-- Discover all fixtures once. +local function list_fixtures() + local entries = fio.listdir(FIXTURES_DIR) + table.sort(entries) + local fixtures = {} + for _, name in ipairs(entries) do + if name:match('%.bin$') then + local base = name:sub(1, -5) + local bin_path = fio.pathjoin(FIXTURES_DIR, name) + local txt_path = fio.pathjoin(FIXTURES_DIR, base .. '.txtpb') + fixtures[#fixtures + 1] = { + name = base, + bin_path = bin_path, + txt_path = txt_path, + full_name = read_type(txt_path), + } + end + end + return fixtures +end + +local FIXTURES = list_fixtures() + +for _, mode in ipairs({'full', 'runtime'}) do + local hello = require(mode .. '.hello.hello_pb') + local g = t.group('interop.' .. mode) + + for _, fx in ipairs(FIXTURES) do + g['test_' .. fx.name] = function() + local golden = slurp(fx.bin_path) + local encode_fn, decode_fn = lua_type_funcs(hello, fx.full_name) + t.assert(encode_fn, 'no encoder for ' .. fx.full_name) + t.assert(decode_fn, 'no decoder for ' .. fx.full_name) + + local decoded = decode_fn(golden) + local reencoded = encode_fn(decoded) + + t.assert_equals(hex(reencoded), hex(golden), + ('byte-for-byte mismatch with protoc on %s'):format(fx.name)) + end + end +end diff --git a/test/json_test.lua b/test/json_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..9841828075bd36c20ade001d9a9ec3f996b045aa --- /dev/null +++ b/test/json_test.lua @@ -0,0 +1,233 @@ +-- 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 diff --git a/test/protobuf_test.lua b/test/protobuf_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..82fbcf55791207ab06799bc1551894c750b73b77 --- /dev/null +++ b/test/protobuf_test.lua @@ -0,0 +1,439 @@ +-- Round-trip + parity tests for both codegen modes. +-- Run via `make test` (which sets LUA_PATH and invokes .rocks/bin/luatest). +local t = require('luatest') +local ffi = require('ffi') + +local function eq_uint64(a, b) + return ffi.cast('uint64_t', a) == ffi.cast('uint64_t', b) +end + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +-- --------------------------------------------------------------------------- +-- One luatest group per mode, each running the same suite. +-- --------------------------------------------------------------------------- + +local MODES = {'full', 'runtime'} + +for _, mode in ipairs(MODES) do + local g = t.group('hello.' .. mode) + local hello = require(mode .. '.hello.hello_pb') + + g.test_empty_address_round_trips_to_empty = function() + local enc = hello.Address_encode({}) + t.assert_equals(#enc, 0, 'proto3 default-elision: empty message -> 0 bytes') + t.assert_equals(hello.Address_decode(enc), {}) + end + + g.test_address_scalars = function() + local addr = {street = 'Pushkina 1', city = 'Moscow', zip = 123456} + local dec = hello.Address_decode(hello.Address_encode(addr)) + t.assert_equals(dec.street, addr.street) + t.assert_equals(dec.city, addr.city) + t.assert_equals(dec.zip, addr.zip) + end + + g.test_person_basic_round_trip = function() + local p = { + name = 'Alice', + age = 30, + emails = {'a@example.com', 'b@example.com'}, + status = hello.Status.OK, + address = {street = 'Main St', city = 'Springfield', zip = 100}, + } + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.name, 'Alice') + t.assert_equals(dec.age, 30) + t.assert_equals(#dec.emails, 2) + t.assert_equals(dec.emails[1], 'a@example.com') + t.assert_equals(dec.emails[2], 'b@example.com') + t.assert_equals(dec.status, hello.Status.OK) + t.assert_equals(dec.address.street, 'Main St') + t.assert_equals(dec.address.zip, 100) + end + + g.test_self_reference_repeated_message = function() + local p = { + name = 'Root', + friends = { + {name = 'Alice', age = 25}, + {name = 'Bob', age = 31, friends = {{name = 'Carol'}}}, + }, + } + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(#dec.friends, 2) + t.assert_equals(dec.friends[1].name, 'Alice') + t.assert_equals(dec.friends[2].name, 'Bob') + t.assert_equals(dec.friends[2].friends[1].name, 'Carol') + end + + g.test_packed_repeated_int32 = function() + local p = {lucky_numbers = {1, 2, 3, -7, 4, 1024}} + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(#dec.lucky_numbers, 6) + t.assert_equals(dec.lucky_numbers[4], -7) + t.assert_equals(dec.lucky_numbers[6], 1024) + end + + g.test_bytes_sint32_fixed64_double = function() + local p = { + avatar = '\x00\x01\x02\xff', + balance = -12345, + user_id = ffi.cast('uint64_t', 0xfeedface00000001ULL), + weight_kg = 72.5, + } + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.avatar, p.avatar) + t.assert_equals(dec.balance, -12345) + t.assert(eq_uint64(dec.user_id, p.user_id), 'fixed64 round-trips losslessly') + t.assert_almost_equals(dec.weight_kg, 72.5, 1e-9) + end + + g.test_known_good_wire_bytes_alice30 = function() + -- Byte-for-byte conformance with mainline protoc. + -- field 1 (string, LEN): tag 0x0a, len 5, "Alice" = 41 6c 69 63 65 + -- field 2 (int32, VARINT): tag 0x10, value 30 = 0x1e + local enc = hello.Person_encode({name = 'Alice', age = 30}) + t.assert_equals(hex(enc), '0a05416c696365101e') + end + + g.test_enum_string_input_resolved_to_int = function() + local p = hello.Person_decode(hello.Person_encode({name = 'X', status = 'ERROR'})) + t.assert_equals(p.status, hello.Status.ERROR) + end + + g.test_enum_unknown_string_errors = function() + t.assert_error(function() hello.Person_encode({status = 'NOT_A_VALUE'}) end) + end + + g.test_proto3_default_value_elision = function() + t.assert_equals(#hello.Person_encode({age = 0}), 0) + t.assert_equals(#hello.Person_encode({name = ''}), 0) + t.assert_equals(#hello.Person_encode({status = hello.Status.UNKNOWN}), 0) + t.assert_equals(#hello.Person_encode({weight_kg = 0.0}), 0) + t.assert_equals(#hello.Person_encode({avatar = ''}), 0) + end + + g.test_unknown_field_is_skipped = function() + -- Manually craft bytes with an unknown field 12 (VARINT, single-byte tag), + -- then a known field 1 (name = "hi"). Decoder must skip the unknown. + local raw = string.char(12 * 8 + 0) .. '\x05' -- field 12 varint = 5 + .. '\x0a\x02hi' -- field 1 LEN = "hi" + local dec = hello.Person_decode(raw) + t.assert_equals(dec.name, 'hi') + end + + g.test_empty_repeated_field_omitted = function() + -- An empty Lua array shouldn't emit a packed empty payload. + t.assert_equals(#hello.Person_encode({lucky_numbers = {}}), 0) + end + + g.test_map_string_to_int32 = function() + local p = {ages_by_nickname = {alice = 30, bob = 25}} + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.ages_by_nickname.alice, 30) + t.assert_equals(dec.ages_by_nickname.bob, 25) + end + + g.test_map_int32_to_string = function() + local p = {nickname_by_age = {[30] = 'alice', [25] = 'bob'}} + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.nickname_by_age[30], 'alice') + t.assert_equals(dec.nickname_by_age[25], 'bob') + end + + g.test_map_string_to_message = function() + local p = {addresses_by_label = { + home = {street = 'Main', city = 'X', zip = 1}, + work = {street = '5th', city = 'Y', zip = 2}, + }} + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.addresses_by_label.home.street, 'Main') + t.assert_equals(dec.addresses_by_label.home.zip, 1) + t.assert_equals(dec.addresses_by_label.work.street, '5th') + t.assert_equals(dec.addresses_by_label.work.zip, 2) + end + + g.test_map_empty_is_elided = function() + t.assert_equals(#hello.Person_encode({ages_by_nickname = {}}), 0) + end + + g.test_map_defaults_round_trip = function() + -- Empty string key + zero value should survive the round trip. + local p = {ages_by_nickname = {[''] = 0}} + local dec = hello.Person_decode(hello.Person_encode(p)) + t.assert_equals(dec.ages_by_nickname[''], 0) + end + + g.test_wkt_timestamp_round_trip = function() + local datetime = require('datetime') + local dt = datetime.new({timestamp = 1700000000, nsec = 123456789}) + local e = {title = 'hi', created_at = dt} + local dec = hello.Event_decode(hello.Event_encode(e)) + t.assert_equals(dec.title, 'hi') + t.assert(datetime.is_datetime(dec.created_at)) + t.assert_equals(dec.created_at.epoch, 1700000000) + t.assert_equals(dec.created_at.nsec, 123456789) + end + + g.test_wkt_timestamp_table_input = function() + local datetime = require('datetime') + local dec = hello.Event_decode(hello.Event_encode({ + created_at = {seconds = 42, nanos = 500}, + })) + t.assert(datetime.is_datetime(dec.created_at)) + t.assert_equals(dec.created_at.epoch, 42) + t.assert_equals(dec.created_at.nsec, 500) + end + + g.test_wkt_duration = function() + local dec = hello.Event_decode(hello.Event_encode({ + duration = {seconds = 7200, nanos = 0}, + })) + t.assert_equals(tonumber(dec.duration.seconds), 7200) + t.assert_equals(dec.duration.nanos, 0) + end + + g.test_wkt_empty = function() + local dec = hello.Event_decode(hello.Event_encode({ack = {}})) + t.assert_equals(type(dec.ack), 'table') + t.assert_equals(next(dec.ack), nil) + end + + g.test_wkt_int32value_wrapper = function() + -- Wrapper auto-wraps: user passes the unwrapped value, no nesting. + local dec = hello.Event_decode(hello.Event_encode({retry_count = 5})) + t.assert_equals(dec.retry_count, 5) + + -- nil means "not set" — wrapper field omitted entirely. + local dec_nil = hello.Event_decode(hello.Event_encode({})) + t.assert_equals(dec_nil.retry_count, nil) + + -- Zero is the point of wrappers — presence preserved even at default. + local dec0 = hello.Event_decode(hello.Event_encode({retry_count = 0})) + t.assert_equals(dec0.retry_count, 0, + 'zero round-trips through a wrapper (presence is meaningful)') + end + + g.test_wkt_stringvalue_wrapper = function() + local dec = hello.Event_decode(hello.Event_encode({note = 'remember'})) + t.assert_equals(dec.note, 'remember') + end + + g.test_wkt_boolvalue_wrapper = function() + local dec = hello.Event_decode(hello.Event_encode({is_admin = true})) + t.assert_equals(dec.is_admin, true) + end + + g.test_explicit_optional_default_value_round_trips = function() + -- Explicit `optional string apartment = 4`. Setting it to '' must + -- survive the round trip — presence is meaningful. + local enc = hello.Address_encode({apartment = ''}) + t.assert_equals(#enc, 2, 'apartment="" emits tag 0x22 + len 0') + local dec = hello.Address_decode(enc) + t.assert_equals(dec.apartment, '') + t.assert(hello.Address_has_apartment(dec)) + end + + g.test_explicit_optional_unset_omitted = function() + -- nil means "not set"; encoder emits nothing. + local enc = hello.Address_encode({}) + t.assert_equals(#enc, 0) + local dec = hello.Address_decode(enc) + t.assert_equals(dec.apartment, nil) + t.assert(not hello.Address_has_apartment(dec)) + end + + g.test_explicit_optional_set_value = function() + local enc = hello.Address_encode({apartment = '5B'}) + local dec = hello.Address_decode(enc) + t.assert_equals(dec.apartment, '5B') + t.assert(hello.Address_has_apartment(dec)) + end + + g.test_explicit_optional_clear_helper = function() + local addr = {street = 'X', apartment = '5B'} + hello.Address_clear_apartment(addr) + t.assert_equals(addr.apartment, nil) + t.assert(not hello.Address_has_apartment(addr)) + end + + g.test_oneof_text_branch = function() + local r = {id = 1, text = 'hello'} + local dec = hello.Result_decode(hello.Result_encode(r)) + t.assert_equals(dec.id, 1) + t.assert_equals(dec.text, 'hello') + t.assert_equals(dec.code, nil) + t.assert_equals(dec.details, nil) + end + + g.test_oneof_code_branch = function() + local r = {id = 2, code = 42} + local dec = hello.Result_decode(hello.Result_encode(r)) + t.assert_equals(dec.id, 2) + t.assert_equals(dec.code, 42) + t.assert_equals(dec.text, nil) + t.assert_equals(dec.details, nil) + end + + g.test_oneof_message_branch = function() + local r = {id = 3, details = {street = 'X', zip = 99}} + local dec = hello.Result_decode(hello.Result_encode(r)) + t.assert_equals(dec.details.street, 'X') + t.assert_equals(dec.details.zip, 99) + t.assert_equals(dec.text, nil) + t.assert_equals(dec.code, nil) + end + + g.test_oneof_emits_default_value_when_active = function() + -- text='' is the proto3 default for string, but presence is meaningful + -- inside an oneof. The encoder must emit the field anyway. + local enc = hello.Result_encode({text = ''}) + -- Expect: tag for field 2 + len 0 = "\x12\x00" (no field 1 since id=0). + t.assert_equals(#enc, 2) + local dec = hello.Result_decode(enc) + t.assert_equals(dec.text, '') + end + + g.test_oneof_decode_clears_siblings = function() + -- Manually craft bytes setting first text, then code. Decoder must + -- end up with code set and text cleared (last branch wins per spec). + local raw = '\x12\x03foo' -- field 2 (text) LEN=3, "foo" + .. '\x18\x07' -- field 3 (code) varint 7 + local dec = hello.Result_decode(raw) + t.assert_equals(dec.code, 7) + t.assert_equals(dec.text, nil) + end + + g.test_oneof_last_set_wins_on_encode = function() + -- Caller sets multiple branches; encoder picks the LAST in declaration order. + local enc = hello.Result_encode({text = 'first', code = 9, details = {street = 'last'}}) + local dec = hello.Result_decode(enc) + t.assert_equals(dec.details.street, 'last') + t.assert_equals(dec.text, nil) + t.assert_equals(dec.code, nil) + end + + g.test_repeated_string_non_packed = function() + -- Strings are LEN-typed and never packable. Each element gets its own tag. + local enc = hello.Person_encode({emails = {'a', 'b', 'c'}}) + -- 3 occurrences of: tag(field=3, LEN)=0x1a, len=1, ascii. + t.assert_equals(hex(enc), '1a01611a01621a0163') + end +end + +-- --------------------------------------------------------------------------- +-- gRPC: per-mode service round-trip via the loopback transport. +-- --------------------------------------------------------------------------- + +for _, mode in ipairs(MODES) do + local g = t.group('grpc.' .. mode) + local pb = require('pb') + local hello = require(mode .. '.hello.hello_pb') + + g.test_service_descriptor = function() + local svc = hello.Greeter_service + t.assert_equals(svc.name, 'hello.Greeter') + t.assert_equals(svc.methods.SayHello.full_name, '/hello.Greeter/SayHello') + t.assert_equals(svc.methods.SayHello.input, hello.HelloRequest_descriptor) + t.assert_equals(svc.methods.SayHello.output, hello.HelloReply_descriptor) + t.assert_equals(svc.methods.StreamHellos.server_streaming, true) + end + + g.test_unary_round_trip_via_loopback = function() + local impl = { + SayHello = function(req, _) + return {greeting = 'Hello, ' .. req.name} + end, + Echo = function(req, _) + return req + end, + } + local server = hello.Greeter_server(impl) + local client = hello.Greeter_client(pb.grpc.loopback(server)) + + local reply = client.SayHello({name = 'World'}, {}) + t.assert_equals(reply.greeting, 'Hello, World') + + local echoed = client.Echo({name = 'ping'}, {}) + t.assert_equals(echoed.name, 'ping') + end + + g.test_missing_handler_errors_clearly = function() + local server = hello.Greeter_server({}) -- no implementations + local client = hello.Greeter_client(pb.grpc.loopback(server)) + local ok, err = pcall(client.SayHello, {name = 'x'}, {}) + t.assert(not ok) + t.assert_str_contains(tostring(err), 'SayHello: handler missing') + end + + g.test_client_requires_transport = function() + t.assert_error(function() hello.Greeter_client(nil) end) + end + + g.test_server_requires_impl_table = function() + t.assert_error(function() hello.Greeter_server(nil) end) + end +end + +-- --------------------------------------------------------------------------- +-- Cross-mode parity: any silent divergence between full and runtime trips here. +-- --------------------------------------------------------------------------- +local g_parity = t.group('parity.full_vs_runtime') +local hello_full = require('full.hello.hello_pb') +local hello_runtime = require('runtime.hello.hello_pb') + +g_parity.test_address_byte_equality = function() + local samples = { + {}, + {street = 'X'}, + {street = 'X', city = 'Y', zip = 42}, + {zip = -1}, + } + for i, s in ipairs(samples) do + t.assert_equals( + hex(hello_full.Address_encode(s)), + hex(hello_runtime.Address_encode(s)), + ('Address sample #%d diverges'):format(i)) + end +end + +g_parity.test_person_byte_equality = function() + local samples = { + {}, + {name = 'A'}, + {name = 'A', age = 7}, + {emails = {'a', 'b'}}, + {lucky_numbers = {1, 2, 3, -7, 4}}, + {friends = {{name = 'F1'}, {name = 'F2', age = 9}}}, + {avatar = '\x00\x01\xff', user_id = ffi.cast('uint64_t', 1234567890123ULL)}, + {balance = -99999, weight_kg = 3.14159}, + } + for i, s in ipairs(samples) do + t.assert_equals( + hex(hello_full.Person_encode(s)), + hex(hello_runtime.Person_encode(s)), + ('Person sample #%d diverges'):format(i)) + end +end + +g_parity.test_decode_round_trips_through_either_module = function() + -- Encode with full, decode with runtime, and vice versa. + local p = {name = 'P', age = 5, emails = {'a'}, status = hello_full.Status.OK, + friends = {{name = 'F'}}, lucky_numbers = {10, 20}} + local enc = hello_full.Person_encode(p) + local dec = hello_runtime.Person_decode(enc) + t.assert_equals(dec.name, 'P') + t.assert_equals(dec.age, 5) + t.assert_equals(dec.emails[1], 'a') + t.assert_equals(dec.status, hello_full.Status.OK) + t.assert_equals(dec.friends[1].name, 'F') + t.assert_equals(#dec.lucky_numbers, 2) + + local enc2 = hello_runtime.Person_encode(p) + t.assert_equals(hex(enc), hex(enc2)) +end diff --git a/test/struct_value_test.lua b/test/struct_value_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..1ab6ffc84ba07191a99466bf89e2ead90f9a9618 --- /dev/null +++ b/test/struct_value_test.lua @@ -0,0 +1,215 @@ +-- Tests for google.protobuf.Struct / Value / ListValue. +-- Lua surface: +-- * box.NULL ↔ null_value +-- * boolean ↔ bool_value +-- * number ↔ number_value (double) +-- * string ↔ string_value +-- * table (hash-like) ↔ Struct +-- * table (array-like or pb.wkt.list-tagged) ↔ ListValue +local t = require('luatest') +local pb = require('pb') +local wkt = pb.wkt + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +-- --------------------------------------------------------------------------- +-- Direct Value wire format +-- --------------------------------------------------------------------------- +local g = t.group('struct_value.wire') + +g.test_null_round_trip = function() + local enc = wkt.Value_encode(pb.NULL) + t.assert_equals(hex(enc), '0800', + 'NULL encodes as field 1 (null_value) varint 0') + t.assert_equals(wkt.Value_decode(enc), pb.NULL) +end + +g.test_bool_branches = function() + t.assert_equals(hex(wkt.Value_encode(true)), '2001') + t.assert_equals(hex(wkt.Value_encode(false)), '2000') + t.assert_equals(wkt.Value_decode(wkt.Value_encode(true)), true) + t.assert_equals(wkt.Value_decode(wkt.Value_encode(false)), false) +end + +g.test_number_round_trip = function() + local samples = {0, 1, -1, 3.14159, 1e100, -1e-10, math.huge} + for _, n in ipairs(samples) do + local r = wkt.Value_decode(wkt.Value_encode(n)) + if n ~= n then + t.assert(r ~= r, 'NaN round trip preserves NaN-ness') + else + t.assert_equals(r, n) + end + end +end + +g.test_string_round_trip = function() + for _, s in ipairs({'', 'hello', 'unicode: ünîcö∂é', string.rep('x', 1024)}) do + t.assert_equals(wkt.Value_decode(wkt.Value_encode(s)), s) + end +end + +g.test_empty_buf_decodes_as_null = function() + -- Empty Value{} on wire has no kind set → conventional reading is NULL. + t.assert_equals(wkt.Value_decode(''), pb.NULL) +end + +g.test_unknown_field_in_value_falls_back_to_null = function() + -- A Value with only an unknown field id (e.g. field 99 varint). + local buf = '\x98\x06\x2a' -- tag(99, varint), value 42 + t.assert_equals(wkt.Value_decode(buf), pb.NULL) +end + +-- --------------------------------------------------------------------------- +-- Struct +-- --------------------------------------------------------------------------- +local gs = t.group('struct_value.struct') + +gs.test_empty_struct = function() + t.assert_equals(wkt.Struct_encode({}), '') + local dec = wkt.Struct_decode('') + t.assert_equals(next(dec), nil) +end + +gs.test_scalar_struct_round_trip = function() + local s = {name = 'Alice', age = 30, active = true, deleted = pb.NULL} + local dec = wkt.Struct_decode(wkt.Struct_encode(s)) + t.assert_equals(dec.name, 'Alice') + t.assert_equals(dec.age, 30) + t.assert_equals(dec.active, true) + t.assert_equals(dec.deleted, pb.NULL) +end + +gs.test_nested_struct = function() + local s = {outer = {inner = {leaf = 42}}} + local dec = wkt.Struct_decode(wkt.Struct_encode(s)) + t.assert_equals(dec.outer.inner.leaf, 42) +end + +-- --------------------------------------------------------------------------- +-- ListValue +-- --------------------------------------------------------------------------- +local gl = t.group('struct_value.list') + +gl.test_empty_list = function() + t.assert_equals(wkt.ListValue_encode(wkt.list({})), '') + local dec = wkt.ListValue_decode('') + t.assert_equals(#dec, 0) +end + +gl.test_mixed_element_list_round_trip = function() + local list = wkt.list({1, 'two', true, pb.NULL, wkt.struct({k = 'v'})}) + local dec = wkt.ListValue_decode(wkt.ListValue_encode(list)) + t.assert_equals(#dec, 5) + t.assert_equals(dec[1], 1) + t.assert_equals(dec[2], 'two') + t.assert_equals(dec[3], true) + t.assert_equals(dec[4], pb.NULL) + t.assert_equals(dec[5].k, 'v') +end + +gl.test_array_like_table_auto_routes_to_list = function() + -- A plain {1,2,3} (no tagging) is detected as list because t[1] ~= nil. + local v_buf = wkt.Value_encode({10, 20, 30}) + local dec = wkt.Value_decode(v_buf) + t.assert_equals(type(dec), 'table') + t.assert_equals(#dec, 3) + t.assert_equals(dec[2], 20) +end + +gl.test_empty_table_routes_to_struct = function() + -- Empty {} → Struct (more common dict-like case). + local v_buf = wkt.Value_encode({}) + -- Tag should be 0x2a (field 5, struct_value). + t.assert_equals(v_buf:byte(1), 0x2a) +end + +-- --------------------------------------------------------------------------- +-- Round-trip Value through wire then back; verify Struct/List tagging survives. +-- --------------------------------------------------------------------------- +local gt = t.group('struct_value.tagging') + +gt.test_decoded_struct_round_trips_byte_equal = function() + local original = wkt.struct({a = 1, b = 'hi'}) + local enc1 = wkt.Value_encode(original) + local roundtrip = wkt.Value_decode(enc1) + local enc2 = wkt.Value_encode(roundtrip) + t.assert_equals(hex(enc1), hex(enc2)) +end + +gt.test_decoded_empty_list_round_trips_byte_equal = function() + -- Without tagging, an empty list would re-encode as struct. Decoded + -- ListValue keeps its LIST_MT so the next encode reproduces the wire. + local enc1 = wkt.Value_encode(wkt.list({})) + local roundtrip = wkt.Value_decode(enc1) + local enc2 = wkt.Value_encode(roundtrip) + t.assert_equals(hex(enc1), hex(enc2)) +end + +-- --------------------------------------------------------------------------- +-- End-to-end: generated Event message exercising payload/attribute/tags. +-- --------------------------------------------------------------------------- +for _, mode in ipairs({'full', 'runtime'}) do + local ge = t.group('struct_value.event.' .. mode) + local hello = require(mode .. '.hello.hello_pb') + + ge.test_event_with_struct_and_list = function() + local e = { + title = 'launch', + payload = wkt.struct({region = 'eu-west-1', priority = 2, urgent = true}), + attribute = 'experimental', + tags = wkt.list({'alpha', 'beta', 99}), + } + local dec = hello.Event_decode(hello.Event_encode(e)) + t.assert_equals(dec.title, 'launch') + t.assert_equals(dec.payload.region, 'eu-west-1') + t.assert_equals(dec.payload.priority, 2) + t.assert_equals(dec.payload.urgent, true) + t.assert_equals(dec.attribute, 'experimental') + t.assert_equals(#dec.tags, 3) + t.assert_equals(dec.tags[1], 'alpha') + t.assert_equals(dec.tags[3], 99) + end + + ge.test_event_attribute_with_null = function() + local e = {title = 'x', attribute = pb.NULL} + local dec = hello.Event_decode(hello.Event_encode(e)) + t.assert_equals(dec.attribute, pb.NULL) + end + + ge.test_event_with_nested_struct = function() + local e = { + payload = wkt.struct({ + stats = wkt.struct({calls = 7, errors = 0}), + hosts = wkt.list({'a.example', 'b.example'}), + }), + } + local dec = hello.Event_decode(hello.Event_encode(e)) + t.assert_equals(dec.payload.stats.calls, 7) + t.assert_equals(dec.payload.stats.errors, 0) + t.assert_equals(#dec.payload.hosts, 2) + t.assert_equals(dec.payload.hosts[2], 'b.example') + end +end + +-- --------------------------------------------------------------------------- +-- Parity: full and runtime mode produce identical bytes for Event. +-- --------------------------------------------------------------------------- +local gp = t.group('struct_value.parity') +local hello_full = require('full.hello.hello_pb') +local hello_runtime = require('runtime.hello.hello_pb') + +gp.test_event_bytes_match = function() + local e = { + title = 'parity', + payload = wkt.struct({k = 'v', n = 42, flag = pb.NULL}), + attribute = wkt.list({1, 2, 3}), + tags = wkt.list({'x', 'y'}), + } + t.assert_equals(hex(hello_full.Event_encode(e)), + hex(hello_runtime.Event_encode(e))) +end diff --git a/test/unknown_test.lua b/test/unknown_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..198fff8059cd71d3a9930c40b115e34373f74b07 --- /dev/null +++ b/test/unknown_test.lua @@ -0,0 +1,127 @@ +-- Unknown-field passthrough: a decoder that meets fields not in its schema +-- must capture their raw bytes into result._unknown_fields and a subsequent +-- encode must re-emit them verbatim. Mirrors Tarantool's built-in `protobuf` +-- module convention (see PLAN.md §4.5). +local t = require('luatest') + +local function hex(s) + local out = {} + for i = 1, #s do out[i] = string.format('%02x', s:byte(i)) end + return table.concat(out) +end + +local MODES = {'full', 'runtime'} + +-- Wire-tag for (field_id, wire_type). Single-byte for id <= 15; we always +-- produce the multi-byte varint here for safety. +local function tag_bytes(id, wt) + local v = id * 8 + wt + local out = {} + while v >= 0x80 do + out[#out + 1] = string.char(v % 0x80 + 0x80) + v = math.floor(v / 0x80) + end + out[#out + 1] = string.char(v) + return table.concat(out) +end + +-- Construct one byte-slice per wire type, using field IDs not declared in +-- examples/proto/hello.proto's Address (fields 1..4). These should all be +-- treated as unknown by the Address decoder. +local UNK_VARINT = tag_bytes(50, 0) .. '\x2a' -- value 42 +local UNK_I32 = tag_bytes(51, 5) .. '\x01\x00\x00\x00' -- value 1 +local UNK_I64 = tag_bytes(52, 1) .. '\x02\x00\x00\x00\x00\x00\x00\x00' +local UNK_LEN = tag_bytes(53, 2) .. '\x03foo' -- 3-byte string + +local ALL_UNK = UNK_VARINT .. UNK_I32 .. UNK_I64 .. UNK_LEN + +for _, mode in ipairs(MODES) do + local g = t.group('unknown.' .. mode) + local hello = require(mode .. '.hello.hello_pb') + + g.test_no_unknown_means_field_absent = function() + local dec = hello.Address_decode(hello.Address_encode({street = 'X'})) + t.assert_equals(dec.street, 'X') + t.assert_equals(dec._unknown_fields, nil, + '_unknown_fields must be absent when input had only known fields') + end + + g.test_varint_unknown_round_trips = function() + local known = hello.Address_encode({street = 'X', zip = 7}) + local mixed = known .. UNK_VARINT + local dec = hello.Address_decode(mixed) + t.assert_equals(dec.street, 'X') + t.assert_equals(dec.zip, 7) + t.assert_equals(hex(dec._unknown_fields), hex(UNK_VARINT)) + end + + g.test_all_wire_types_captured_in_order = function() + local known = hello.Address_encode({street = 'X'}) + local mixed = known .. ALL_UNK + local dec = hello.Address_decode(mixed) + t.assert_equals(dec.street, 'X') + t.assert_equals(hex(dec._unknown_fields), hex(ALL_UNK), + 'all four wire types must be captured verbatim in source order') + end + + g.test_unknowns_interleaved_with_knowns = function() + -- Bytes ordering: unknown, known, unknown — capture must preserve the + -- two unknown chunks in encounter order (the known field stays out). + local known1 = hello.Address_encode({street = 'A'}) + local known2 = hello.Address_encode({zip = 99}) + local mixed = UNK_VARINT .. known1 .. UNK_I64 .. known2 + local dec = hello.Address_decode(mixed) + t.assert_equals(dec.street, 'A') + t.assert_equals(dec.zip, 99) + t.assert_equals(hex(dec._unknown_fields), hex(UNK_VARINT .. UNK_I64)) + end + + g.test_re_encode_preserves_unknown_bytes = function() + local mixed = hello.Address_encode({street = 'X'}) .. ALL_UNK + local dec = hello.Address_decode(mixed) + local re_enc = hello.Address_encode(dec) + -- Knowns are re-encoded in field-declaration order; unknowns trail. + local expected = hello.Address_encode({street = 'X'}) .. ALL_UNK + t.assert_equals(hex(re_enc), hex(expected), + 'unknown bytes must be re-emitted verbatim at the tail') + end + + g.test_re_encode_decode_idempotent = function() + local mixed = hello.Address_encode({street = 'X', zip = 1}) .. UNK_LEN + local dec1 = hello.Address_decode(mixed) + local dec2 = hello.Address_decode(hello.Address_encode(dec1)) + t.assert_equals(dec2.street, 'X') + t.assert_equals(dec2.zip, 1) + t.assert_equals(hex(dec2._unknown_fields), hex(UNK_LEN)) + end + + g.test_empty_unknown_string_treated_as_absent = function() + -- User explicitly sets _unknown_fields = '' on encode; should be a no-op. + local enc = hello.Address_encode({street = 'X', _unknown_fields = ''}) + t.assert_equals(hex(enc), hex(hello.Address_encode({street = 'X'}))) + end +end + +-- --------------------------------------------------------------------------- +-- Cross-mode parity: full and runtime must capture identical unknown bytes. +-- --------------------------------------------------------------------------- +local g_parity = t.group('unknown.parity') +local hello_full = require('full.hello.hello_pb') +local hello_runtime = require('runtime.hello.hello_pb') + +g_parity.test_capture_identical_across_modes = function() + local mixed = hello_full.Address_encode({street = 'X'}) .. ALL_UNK + local d1 = hello_full.Address_decode(mixed) + local d2 = hello_runtime.Address_decode(mixed) + t.assert_equals(hex(d1._unknown_fields), hex(d2._unknown_fields)) + t.assert_equals(hex(d1._unknown_fields), hex(ALL_UNK)) +end + +g_parity.test_reencode_identical_across_modes = function() + local table_with_unknowns = { + street = 'X', zip = 9, _unknown_fields = ALL_UNK, + } + t.assert_equals( + hex(hello_full.Address_encode(table_with_unknowns)), + hex(hello_runtime.Address_encode(table_with_unknowns))) +end