From 7676fdc2fdd14066e363732e504a8f640a01cab3 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sun, 17 May 2026 07:46:02 +0300 Subject: [PATCH] codegen: preserve descriptor options as `options = { ... }` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every populated *Options message — FileOptions, MessageOptions, FieldOptions, OneofOptions, EnumOptions, EnumValueOptions, ServiceOptions, MethodOptions — surfaces on the generated descriptor as a plain Lua sub-table named `options` (or `oneof_options` / `value_options` for the per-member shapes). Standard fields use their proto name as a bare Lua key; extensions use their fully-qualified extension name as a bracket-quoted string key. The walker is generic — no extension-specific code paths. Consumers pull whatever they care about: `(google.api.http)` for REST routing, `(versionpb.etcd_version_*)` for compatibility gates, `[deprecated = true]` for migration tooling, and any in-house extension without pb knowing about them. Standard fields sort alphabetically before extensions (also alphabetical by full name) so codegen output stays byte-identical across runs. The `options` key is only emitted when at least one field is populated, so option-free protos produce zero-diff output to before. Resolver re-links *Options* messages so in-file extensions surface via the protoreflect walker — protogen builds f.Desc before in-file extensions are registered, and only f.Proto gets the post-pass fix-up, so we rebuild the resolver manually. UninterpretedOption is treated as a codegen-time error: a populated entry means protoc couldn't resolve the extension, and emitting opaque parser state would hide the problem. --- PLAN.md | 13 + README.md | 38 ++ .../internal/gen/descopts.go | 344 ++++++++++++++++++ cmd/protoc-gen-tarantool/internal/gen/gen.go | 93 ++++- .../internal/gen/service.go | 6 + .../full/conformance/conformance_pb.lua | 2 + examples/expected/full/hello/hello_pb.lua | 2 + .../proto3/test_messages_proto3_pb.lua | 67 ++-- .../full/quickstart/quickstart_pb.lua | 2 + .../runtime/conformance/conformance_pb.lua | 2 + examples/expected/runtime/hello/hello_pb.lua | 2 + .../proto3/test_messages_proto3_pb.lua | 67 ++-- .../runtime/quickstart/quickstart_pb.lua | 2 + test/codegen_options_test.lua | 235 ++++++++++++ 14 files changed, 808 insertions(+), 67 deletions(-) create mode 100644 cmd/protoc-gen-tarantool/internal/gen/descopts.go create mode 100644 test/codegen_options_test.lua diff --git a/PLAN.md b/PLAN.md index 649a077930bb5873d3835dedc58e58d7318c422e..336ad61feefa70590aa798fd47d8a48b441e0a88 100644 --- a/PLAN.md +++ b/PLAN.md @@ -346,6 +346,19 @@ fiber and bridges client ↔ handler via `fiber.channel`. All four flavors `null`, not the string `"NULL_VALUE"`. Null on a NullValue-typed oneof member marks the oneof active. 6. Strict FieldMask round-trip (see M3 entry). +- [x] **Preserve descriptor options.** Every populated `*Options` message + surfaces on the generated descriptor as a plain Lua sub-table named + `options` (or `oneof_options` / `value_options` for oneof / enum value). + Standard fields use their proto name; extensions use their + fully-qualified name (bracket-quoted). The walker is generic — + consumers pull `(google.api.http)`, `(versionpb.etcd_version_*)`, + `[deprecated = true]`, and any in-house extension without pb knowing + about them. Option-free protos emit no `options` keys (byte-identical + output to before). Resolver re-links *Options* messages so in-file + extensions surface via the protoreflect walker (protogen builds + `f.Desc` before in-file extensions are registered; we rebuild + manually). See [docs/codegen.md](docs/codegen.md) for the per-descriptor + shape and the README's "Descriptor options" section for examples. - [x] `protoc-gen-tarantool-doc`: sibling Go plugin under `cmd/protoc-gen-tarantool-doc/` that emits one Markdown file per input `.proto`. Sections: header (package + imports), messages diff --git a/README.md b/README.md index 2e0394fa69965f0bb434187031de07506865e9aa..e5c1f650c8a78a4d01489f38320433e94117860b 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,44 @@ Repeated fields are Lua arrays (1-based, contiguous). 64-bit integers `uint64_t` cdata — lossless and the same convention used by Tarantool's `net.box`, `msgpack`, and built-in `protobuf` modules. +### Descriptor options + +Every populated `*Options` message — `FileOptions`, `MessageOptions`, +`FieldOptions`, `OneofOptions`, `EnumOptions`, `EnumValueOptions`, +`ServiceOptions`, `MethodOptions` (see +[`google/protobuf/descriptor.proto`][descriptor]) — surfaces on the +generated descriptor as a plain Lua sub-table named `options`. Standard +fields use their proto name as a bare Lua key (`deprecated`, `packed`, +`json_name`, …); extensions use their fully-qualified name as a +bracket-quoted string key (`["google.api.http"]`, +`["versionpb.etcd_version_msg"]`, …). The walker is generic — pb has no +opinion about which extensions are interesting; consumers pull whichever +they care about for REST routing (`google.api.http`), version gates +(`versionpb.etcd_version_*`), in-house annotations, and so on. + +The `options` key is **only emitted when at least one field is +populated**, so proto files without any options produce byte-identical +output to before. Message-valued extensions recurse into the same shape: + +```lua +M.Annotated_method = M.Demo_service.methods.Annotated +M.Annotated_method.options -- {deprecated=true, ...} +M.Annotated_method.options["google.api.http"].post -- "/v1/demo" +M.Annotated_method.options["google.api.http"].additional_bindings[1].post +``` + +Per-descriptor key: +- `M._descriptor.options` — `MessageOptions` (+ extensions) +- field's `options` (inline) — `FieldOptions` (+ extensions) +- `M._descriptor.oneof_options` — `{oneof_name = OneofOptions}` +- `M._descriptor.options` — `EnumOptions` +- `M._descriptor.value_options` — `{VALUE_NAME = EnumValueOptions}` +- `M._service.options` — `ServiceOptions` +- method's `options` — `MethodOptions` +- `M.options` — `FileOptions` (incl. `(tarantool.lua_package)`) + +[descriptor]: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/descriptor.proto + ### Migrating from a Lua proto library that auto-down-casts int64 If you're moving from a library that hands back Lua numbers (silently diff --git a/cmd/protoc-gen-tarantool/internal/gen/descopts.go b/cmd/protoc-gen-tarantool/internal/gen/descopts.go new file mode 100644 index 0000000000000000000000000000000000000000..fc76e918031601eb1970605cdc1c493cb2f072c7 --- /dev/null +++ b/cmd/protoc-gen-tarantool/internal/gen/descopts.go @@ -0,0 +1,344 @@ +package gen + +import ( + "fmt" + "math" + "sort" + "strconv" + "strings" + + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/dynamicpb" +) + +// optionsResolver re-links *Options messages so custom extensions defined +// inside an input .proto file are reachable to the protoreflect walker. +// +// Why this is needed: protogen builds each File's descriptor BEFORE in-file +// extensions get registered into its internal resolver. As a result, +// `file.Desc.Options()` (and the same for nested messages / fields / +// services / methods) parses options against a resolver that doesn't yet +// know the in-file extensions — those land silently in the unknown-fields +// bucket and don't surface via `Range`. The Plugin's re-marshal pass +// (`hasNovelExtensions`) only fixes `file.Proto`, which protogen.Message +// & co. don't expose. +// +// Workaround: build a single resolver carrying every extension defined in +// every input file (transitively — nested message extensions included), and +// re-decode each *Options message through it on demand. Marshal + unmarshal +// per options message is cheap (options are tiny). +type optionsResolver struct { + types *protoregistry.Types +} + +func newOptionsResolver(plug *protogen.Plugin) *optionsResolver { + types := new(protoregistry.Types) + for _, f := range plug.Files { + registerExtensions(types, f.Desc) + } + return &optionsResolver{types: types} +} + +// extensionContainer is the common slice of FileDescriptor / MessageDescriptor +// that exposes both extensions and nested messages. +type extensionContainer interface { + Extensions() protoreflect.ExtensionDescriptors + Messages() protoreflect.MessageDescriptors +} + +func registerExtensions(types *protoregistry.Types, c extensionContainer) { + exts := c.Extensions() + for i := 0; i < exts.Len(); i++ { + // Best-effort: an already-registered name (when two input files + // re-import the same extension) returns AlreadyExists, which we + // can safely ignore. + _ = types.RegisterExtension(dynamicpb.NewExtensionType(exts.Get(i))) + } + msgs := c.Messages() + for i := 0; i < msgs.Len(); i++ { + registerExtensions(types, msgs.Get(i)) + } +} + +// relink re-decodes an *Options message through the resolver so populated +// extensions move from the unknown-fields bucket into typed fields. +// Returns nil on a nil input. +func (r *optionsResolver) relink(opts proto.Message) (proto.Message, error) { + if opts == nil { + return nil, nil + } + if r == nil || r.types == nil { + return opts, nil + } + b, err := proto.Marshal(opts) + if err != nil { + return nil, err + } + fresh := opts.ProtoReflect().Type().New().Interface() + if err := (proto.UnmarshalOptions{Resolver: r.types}).Unmarshal(b, fresh); err != nil { + return nil, err + } + return fresh, nil +} + +// renderDescriptorOptions walks every populated field on a descriptor's +// *Options message and emits it as a Lua table literal `{key = value, ...}`. +// +// Returns "" when nothing is populated — callers must omit the `options` +// key entirely in that case (we never emit `options = {}`). +// +// The walker is generic: there are no extension-specific code paths. Every +// extension defined against any *Options message surfaces with the same +// shape, so consumers can pull whatever they care about (google.api.http, +// versionpb.etcd_version_*, in-house extensions) without pb knowing about it. +func renderDescriptorOptions(resolver *optionsResolver, opts proto.Message) (string, error) { + if opts == nil { + return "", nil + } + relinked, err := resolver.relink(opts) + if err != nil { + return "", err + } + msg := relinked.ProtoReflect() + if !msg.IsValid() { + return "", nil + } + return renderOptionsMessage(msg) +} + +// optionEntry holds one populated field for sorted, deterministic emission. +type optionEntry struct { + isExt bool + // sortName: proto field name for standard fields, extension full name + // for extensions. Lets us sort standard-then-extension, each alphabetical. + sortName string + // key is the Lua source for the key (`foo` or `["full.name"]`). + key string + value string +} + +func renderOptionsMessage(msg protoreflect.Message) (string, error) { + var items []optionEntry + var walkErr error + + msg.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + // UninterpretedOption is protoc's parser-internal slot. Once the + // option's defining .proto is in the input set, the option lands + // as a real extension and uninterpreted_option stays empty. A + // populated entry means protoc couldn't resolve the extension — + // surface that explicitly rather than emit opaque parser state. + if !fd.IsExtension() && fd.Name() == "uninterpreted_option" { + walkErr = fmt.Errorf( + "%s carries unresolved UninterpretedOption %q — the "+ + "defining .proto for the extension must be in the "+ + "protoc input set (-I path)", + msg.Descriptor().FullName(), + describeUninterpreted(v)) + return false + } + val, err := renderOptionValue(fd, v) + if err != nil { + walkErr = err + return false + } + var key, sortName string + if fd.IsExtension() { + key = fmt.Sprintf("[%q]", string(fd.FullName())) + sortName = string(fd.FullName()) + } else { + key = luaTableKey(string(fd.Name())) + sortName = string(fd.Name()) + } + items = append(items, optionEntry{ + isExt: fd.IsExtension(), + sortName: sortName, + key: key, + value: val, + }) + return true + }) + + if walkErr != nil { + return "", walkErr + } + if len(items) == 0 { + return "", nil + } + + // Deterministic ordering: standard fields first (alphabetical), then + // extensions (alphabetical by full name). + sort.SliceStable(items, func(i, j int) bool { + if items[i].isExt != items[j].isExt { + return !items[i].isExt + } + return items[i].sortName < items[j].sortName + }) + + parts := make([]string, 0, len(items)) + for _, p := range items { + parts = append(parts, p.key+" = "+p.value) + } + return "{" + strings.Join(parts, ", ") + "}", nil +} + +// renderOptionValue handles cardinality (singular / repeated / map). Singular +// values are dispatched to renderOptionScalar. +func renderOptionValue(fd protoreflect.FieldDescriptor, v protoreflect.Value) (string, error) { + switch { + case fd.IsList(): + list := v.List() + parts := make([]string, list.Len()) + for i := 0; i < list.Len(); i++ { + s, err := renderOptionLeaf(fd, list.Get(i)) + if err != nil { + return "", err + } + parts[i] = s + } + return "{" + strings.Join(parts, ", ") + "}", nil + case fd.IsMap(): + // Map options are rare; emit `{[k] = v, ...}` sorted by stringified + // key for stable output. + m := v.Map() + keyFD := fd.MapKey() + valFD := fd.MapValue() + type kv struct{ k, v string } + var pairs []kv + var err error + m.Range(func(mk protoreflect.MapKey, mv protoreflect.Value) bool { + ks, e := renderOptionLeaf(keyFD, mk.Value()) + if e != nil { + err = e + return false + } + vs, e := renderOptionLeaf(valFD, mv) + if e != nil { + err = e + return false + } + pairs = append(pairs, kv{ks, vs}) + return true + }) + if err != nil { + return "", err + } + sort.Slice(pairs, func(i, j int) bool { return pairs[i].k < pairs[j].k }) + parts := make([]string, len(pairs)) + for i, p := range pairs { + parts[i] = "[" + p.k + "] = " + p.v + } + return "{" + strings.Join(parts, ", ") + "}", nil + } + return renderOptionLeaf(fd, v) +} + +// renderOptionLeaf renders a single (non-list, non-map) value. +func renderOptionLeaf(fd protoreflect.FieldDescriptor, v protoreflect.Value) (string, error) { + switch fd.Kind() { + case protoreflect.BoolKind: + if v.Bool() { + return "true", nil + } + return "false", nil + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return strconv.FormatInt(int64(int32(v.Int())), 10), nil + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return strconv.FormatUint(uint64(uint32(v.Uint())), 10), nil + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + // LuaJIT cdata literal — matches the convention pb decoders use + // for proto3 int64 fields. + return strconv.FormatInt(v.Int(), 10) + "LL", nil + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return strconv.FormatUint(v.Uint(), 10) + "ULL", nil + case protoreflect.FloatKind, protoreflect.DoubleKind: + return formatLuaFloat(v.Float()), nil + case protoreflect.StringKind: + return strconv.Quote(v.String()), nil + case protoreflect.BytesKind: + return luaByteString(v.Bytes()), nil + case protoreflect.EnumKind: + ev := fd.Enum().Values().ByNumber(v.Enum()) + if ev != nil { + return strconv.Quote(string(ev.Name())), nil + } + return strconv.FormatInt(int64(v.Enum()), 10), nil + case protoreflect.MessageKind, protoreflect.GroupKind: + nested, err := renderOptionsMessage(v.Message()) + if err != nil { + return "", err + } + if nested == "" { + // Message-valued option with all fields defaulted — emit `{}` + // so the option's presence remains visible to consumers. + return "{}", nil + } + return nested, nil + } + return "", fmt.Errorf("unsupported option kind: %s", fd.Kind()) +} + +// formatLuaFloat renders a Go float64 as Lua source. NaN/Inf get the +// idiomatic Lua expressions instead of strconv's "NaN"/"+Inf" tokens. +func formatLuaFloat(f float64) string { + switch { + case math.IsNaN(f): + return "0/0" + case math.IsInf(f, 1): + return "math.huge" + case math.IsInf(f, -1): + return "-math.huge" + } + return strconv.FormatFloat(f, 'g', -1, 64) +} + +// mustRenderOptions is the panic-on-error variant for hot codegen sites +// where threading an error return would balloon every emit signature. +// A populated UninterpretedOption (the only error we surface) is a +// codegen-time invariant violation — failing loudly via the plugin's +// panic handler is the right escalation. +func mustRenderOptions(resolver *optionsResolver, opts proto.Message) string { + s, err := renderDescriptorOptions(resolver, opts) + if err != nil { + panic("tarantool-protobuf: " + err.Error()) + } + return s +} + +// describeUninterpreted formats UninterpretedOption.name parts into the +// proto dotted-identifier form so the codegen error tells the user +// exactly which option went unresolved. +func describeUninterpreted(v protoreflect.Value) string { + list := v.List() + if list.Len() == 0 { + return "(empty)" + } + first := list.Get(0).Message() + desc := first.Descriptor() + nameFD := desc.Fields().ByName("name") + if nameFD == nil { + return "(unknown)" + } + parts := first.Get(nameFD).List() + var sb strings.Builder + partDesc := nameFD.Message() + partNameFD := partDesc.Fields().ByName("name_part") + partExtFD := partDesc.Fields().ByName("is_extension") + for i := 0; i < parts.Len(); i++ { + part := parts.Get(i).Message() + if i > 0 { + sb.WriteByte('.') + } + isExt := partExtFD != nil && part.Get(partExtFD).Bool() + if isExt { + sb.WriteByte('(') + } + sb.WriteString(part.Get(partNameFD).String()) + if isExt { + sb.WriteByte(')') + } + } + return sb.String() +} diff --git a/cmd/protoc-gen-tarantool/internal/gen/gen.go b/cmd/protoc-gen-tarantool/internal/gen/gen.go index 0a3efdd3bf7d5b4555cb4514cee3ba72725099c7..314655ed67584ad28bd2d7b0b1f3e06101ee25e5 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/gen.go +++ b/cmd/protoc-gen-tarantool/internal/gen/gen.go @@ -8,6 +8,7 @@ import ( "strings" "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -63,7 +64,7 @@ func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error allEnums := flattenEnums(file.Enums, file.Messages) out := plug.NewGeneratedFile(outputFilename(file.Desc, cfg.Prefix), "") - w := &writer{GeneratedFile: out} + w := &writer{GeneratedFile: out, opts: newOptionsResolver(plug)} emitHeader(w, file) imports := collectImports(file, allMsgs, cfg.Prefix) @@ -72,6 +73,13 @@ func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error w.line("local M = {}") w.line("") + // File-level options (FileOptions + any extensions on it). Includes + // `(tarantool.lua_package)` when set; consumers introspect the rest. + if opts := w.renderOpts(file.Desc.Options()); opts != "" { + w.line("M.options = %s", opts) + w.line("") + } + // 1) Enums first (no forward-ref problems). for _, e := range allEnums { emitEnum(w, file, e) @@ -123,6 +131,9 @@ func GenerateFile(plug *protogen.Plugin, file *protogen.File, cfg Config) error type writer struct { *protogen.GeneratedFile + // opts re-links *Options messages so in-file extensions surface via the + // generic walker — see optionsResolver in descopts.go. + opts *optionsResolver } func (w *writer) line(format string, args ...any) { @@ -133,6 +144,14 @@ func (w *writer) line(format string, args ...any) { } } +// renderOpts is the writer-bound shortcut for rendering a descriptor's +// *Options message into a Lua table literal, threading the writer's +// extension resolver. Returns "" when no fields are populated — caller +// must omit the `options` key entirely. +func (w *writer) renderOpts(opts proto.Message) string { + return mustRenderOptions(w.opts, opts) +} + // ---------------------------------------------------------------------------- // Header & requires // ---------------------------------------------------------------------------- @@ -216,11 +235,41 @@ func emitEnum(w *writer, file *protogen.File, e *protogen.Enum) { w.line(" %s = %d,", string(v.Desc.Name()), v.Desc.Number()) } w.line("})") + if opts := w.renderOpts(e.Desc.Options()); opts != "" { + w.line("M.%s_descriptor.options = %s", name, opts) + } + emitEnumValueOptions(w, name, e) // Convenience aliases the user can reach via `M.MyEnum.RED`, etc. w.line("M.%s = M.%s_descriptor.by_name", name, name) w.line("") } +// emitEnumValueOptions emits `M._descriptor.value_options = +// { NAME = {...}, ... }` when any value carries populated options. Skipped +// otherwise so today's option-free enums stay byte-identical. +func emitEnumValueOptions(w *writer, name string, e *protogen.Enum) { + type row struct { + name string + opts string + } + var rows []row + for _, v := range e.Values { + opts := w.renderOpts(v.Desc.Options()) + if opts == "" { + continue + } + rows = append(rows, row{name: string(v.Desc.Name()), opts: opts}) + } + if len(rows) == 0 { + return + } + w.line("M.%s_descriptor.value_options = {", name) + for _, r := range rows { + w.line(" %s = %s,", luaTableKey(r.name), r.opts) + } + w.line("}") +} + // ---------------------------------------------------------------------------- // Message emission // ---------------------------------------------------------------------------- @@ -234,10 +283,14 @@ func emitMessageFields(w *writer, file *protogen.File, m *protogen.Message, impo 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(" %s,", renderFieldEntry(w, file, f, selfPath, imports, prefix)) } w.line("}") + if opts := w.renderOpts(m.Desc.Options()); opts != "" { + w.line("M.%s_descriptor.options = %s", name, opts) + } emitOneofTable(w, name, m) + emitOneofOptions(w, name, m) emitReservedNames(w, name, m) w.line("pb.finalize_message(M.%s_descriptor)", name) emitFieldNamesTable(w, name, m) @@ -323,6 +376,36 @@ func emitOneofTable(w *writer, name string, m *protogen.Message) { w.line("}") } +// emitOneofOptions emits `M._descriptor.oneof_options = { = {...} }` +// when any non-synthetic oneof carries populated OneofOptions. Skipped when +// no oneof has options to keep generated output identical to today for the +// common case. +func emitOneofOptions(w *writer, name string, m *protogen.Message) { + type row struct { + name string + opts string + } + var rows []row + for _, oo := range m.Oneofs { + if oo.Fields[0].Desc.HasOptionalKeyword() { + continue + } + opts := w.renderOpts(oo.Desc.Options()) + if opts == "" { + continue + } + rows = append(rows, row{name: string(oo.Desc.Name()), opts: opts}) + } + if len(rows) == 0 { + return + } + w.line("M.%s_descriptor.oneof_options = {", name) + for _, r := range rows { + w.line(" %s = %s,", luaTableKey(r.name), r.opts) + } + w.line("}") +} + // emitReservedNames emits `M._descriptor.reserved_names = { ["x"] = true }` // when the message declares any reserved field names. The text-format decoder // uses this to silently drop fields named in `reserved "..."` declarations. @@ -341,7 +424,7 @@ func emitReservedNames(w *writer, name string, m *protogen.Message) { } // 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 { +func renderFieldEntry(w *writer, 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()), @@ -395,6 +478,10 @@ func renderFieldEntry(file *protogen.File, f *protogen.Field, selfPath string, i parts = append(parts, "optional=true") } + if opts := w.renderOpts(f.Desc.Options()); opts != "" { + parts = append(parts, "options="+opts) + } + return "{" + strings.Join(parts, ", ") + "}" } diff --git a/cmd/protoc-gen-tarantool/internal/gen/service.go b/cmd/protoc-gen-tarantool/internal/gen/service.go index 6e8934a32e6d18d9112d01353edc4c16132919d5..c57ea649889e1ac8d5ae7227eca093b55def75eb 100644 --- a/cmd/protoc-gen-tarantool/internal/gen/service.go +++ b/cmd/protoc-gen-tarantool/internal/gen/service.go @@ -57,9 +57,15 @@ func emitService(w *writer, file *protogen.File, svc *protogen.Service, imports if m.Desc.IsStreamingServer() { w.line(" server_streaming = true,") } + if opts := w.renderOpts(m.Desc.Options()); opts != "" { + w.line(" options = %s,", opts) + } w.line(" },") } w.line(" },") + if opts := w.renderOpts(svc.Desc.Options()); opts != "" { + w.line(" options = %s,", opts) + } w.line("}") w.line("") diff --git a/examples/expected/full/conformance/conformance_pb.lua b/examples/expected/full/conformance/conformance_pb.lua index 8b7cf69c991fe6e25932b43b39c9fea6721432ed..df624215fd601f547f653e0435f3f7b7adbbc865 100644 --- a/examples/expected/full/conformance/conformance_pb.lua +++ b/examples/expected/full/conformance/conformance_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/conformance", java_package = "com.google.protobuf.conformance", objc_class_prefix = "Conformance"} + -- Enum: conformance.WireFormat M.WireFormat_descriptor = pb.enum("conformance.WireFormat", { UNSPECIFIED = 0, diff --git a/examples/expected/full/hello/hello_pb.lua b/examples/expected/full/hello/hello_pb.lua index 84754212a555690559dcb271fe91dadbe41c2394..f59d634c0a297d42a60f327baf877f09eb615ec4 100644 --- a/examples/expected/full/hello/hello_pb.lua +++ b/examples/expected/full/hello/hello_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/hello"} + -- Enum: hello.Status M.Status_descriptor = pb.enum("hello.Status", { UNKNOWN = 0, 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 index 32a53e840346076d5660e9adb327cf7d2d64ee96..68f67851f7c6a8e03938f3590e6dd5b96d20e3e8 100644 --- 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 @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {cc_enable_arenas = true, go_package = "tarantoolpb_synthetic/test_messages_proto3", java_package = "com.google.protobuf_test_messages.proto3", objc_class_prefix = "Proto3", optimize_for = "SPEED"} + -- Enum: protobuf_test_messages.proto3.ForeignEnum M.ForeignEnum_descriptor = pb.enum("protobuf_test_messages.proto3.ForeignEnum", { FOREIGN_FOO = 0, @@ -34,6 +36,7 @@ M.TestAllTypesProto3_AliasedEnum_descriptor = pb.enum("protobuf_test_messages.pr moo = 2, bAz = 2, }) +M.TestAllTypesProto3_AliasedEnum_descriptor.options = {allow_alias = true} M.TestAllTypesProto3_AliasedEnum = M.TestAllTypesProto3_AliasedEnum_descriptor.by_name -- Enum: protobuf_test_messages.proto3.EnumOnlyProto3.Bool @@ -72,8 +75,8 @@ M.TestAllTypesProto3_descriptor.fields = { {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="optional_string_piece", id=24, kind='scalar', proto_type="string", options={ctype = "STRING_PIECE"}}, + {name="optional_cord", id=25, kind='scalar', proto_type="string", options={ctype = "CORD"}}, {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}, @@ -94,36 +97,36 @@ M.TestAllTypesProto3_descriptor.fields = { {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="repeated_string_piece", id=54, kind='scalar', proto_type="string", repeated=true, options={ctype = "STRING_PIECE"}}, + {name="repeated_cord", id=55, kind='scalar', proto_type="string", repeated=true, options={ctype = "CORD"}}, + {name="packed_int32", id=75, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}}, + {name="packed_int64", id=76, kind='scalar', proto_type="int64", repeated=true, packed=true, options={packed = true}}, + {name="packed_uint32", id=77, kind='scalar', proto_type="uint32", repeated=true, packed=true, options={packed = true}}, + {name="packed_uint64", id=78, kind='scalar', proto_type="uint64", repeated=true, packed=true, options={packed = true}}, + {name="packed_sint32", id=79, kind='scalar', proto_type="sint32", repeated=true, packed=true, options={packed = true}}, + {name="packed_sint64", id=80, kind='scalar', proto_type="sint64", repeated=true, packed=true, options={packed = true}}, + {name="packed_fixed32", id=81, kind='scalar', proto_type="fixed32", repeated=true, packed=true, options={packed = true}}, + {name="packed_fixed64", id=82, kind='scalar', proto_type="fixed64", repeated=true, packed=true, options={packed = true}}, + {name="packed_sfixed32", id=83, kind='scalar', proto_type="sfixed32", repeated=true, packed=true, options={packed = true}}, + {name="packed_sfixed64", id=84, kind='scalar', proto_type="sfixed64", repeated=true, packed=true, options={packed = true}}, + {name="packed_float", id=85, kind='scalar', proto_type="float", repeated=true, packed=true, options={packed = true}}, + {name="packed_double", id=86, kind='scalar', proto_type="double", repeated=true, packed=true, options={packed = true}}, + {name="packed_bool", id=87, kind='scalar', proto_type="bool", repeated=true, packed=true, options={packed = true}}, + {name="packed_nested_enum", id=88, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true, options={packed = true}}, + {name="unpacked_int32", id=89, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_int64", id=90, kind='scalar', proto_type="int64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_uint32", id=91, kind='scalar', proto_type="uint32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_uint64", id=92, kind='scalar', proto_type="uint64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sint32", id=93, kind='scalar', proto_type="sint32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sint64", id=94, kind='scalar', proto_type="sint64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_fixed32", id=95, kind='scalar', proto_type="fixed32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_fixed64", id=96, kind='scalar', proto_type="fixed64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sfixed32", id=97, kind='scalar', proto_type="sfixed32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sfixed64", id=98, kind='scalar', proto_type="sfixed64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_float", id=99, kind='scalar', proto_type="float", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_double", id=100, kind='scalar', proto_type="double", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_bool", id=101, kind='scalar', proto_type="bool", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_nested_enum", id=102, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=false, options={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"}}, diff --git a/examples/expected/full/quickstart/quickstart_pb.lua b/examples/expected/full/quickstart/quickstart_pb.lua index 2b92d04e07dd948a2a17268d19ddcfa2191e7633..742d565bf46a3b940ba02fca2dfa4810df92f0de 100644 --- a/examples/expected/full/quickstart/quickstart_pb.lua +++ b/examples/expected/full/quickstart/quickstart_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/quickstart"} + -- Enum: quickstart.Role M.Role_descriptor = pb.enum("quickstart.Role", { ROLE_UNSPECIFIED = 0, diff --git a/examples/expected/runtime/conformance/conformance_pb.lua b/examples/expected/runtime/conformance/conformance_pb.lua index 1ce7866ab5ebe12e6c9d11b4e5808ea85e6bf66d..f80bbca3cd87b6bd8c8458394a2605dc4b425352 100644 --- a/examples/expected/runtime/conformance/conformance_pb.lua +++ b/examples/expected/runtime/conformance/conformance_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/conformance", java_package = "com.google.protobuf.conformance", objc_class_prefix = "Conformance"} + -- Enum: conformance.WireFormat M.WireFormat_descriptor = pb.enum("conformance.WireFormat", { UNSPECIFIED = 0, diff --git a/examples/expected/runtime/hello/hello_pb.lua b/examples/expected/runtime/hello/hello_pb.lua index e43741f11f800cb4247216d6e47e1e7eecaabc90..8dcbc39464ce9aed705ffd1b8d63847a6136c0aa 100644 --- a/examples/expected/runtime/hello/hello_pb.lua +++ b/examples/expected/runtime/hello/hello_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/hello"} + -- Enum: hello.Status M.Status_descriptor = pb.enum("hello.Status", { UNKNOWN = 0, 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 index 04ba37a0dc2ae44be867c22f05111d01693aeb91..0b22565d6df05b579edbe5e7d0500b5449447b33 100644 --- 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 @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {cc_enable_arenas = true, go_package = "tarantoolpb_synthetic/test_messages_proto3", java_package = "com.google.protobuf_test_messages.proto3", objc_class_prefix = "Proto3", optimize_for = "SPEED"} + -- Enum: protobuf_test_messages.proto3.ForeignEnum M.ForeignEnum_descriptor = pb.enum("protobuf_test_messages.proto3.ForeignEnum", { FOREIGN_FOO = 0, @@ -34,6 +36,7 @@ M.TestAllTypesProto3_AliasedEnum_descriptor = pb.enum("protobuf_test_messages.pr moo = 2, bAz = 2, }) +M.TestAllTypesProto3_AliasedEnum_descriptor.options = {allow_alias = true} M.TestAllTypesProto3_AliasedEnum = M.TestAllTypesProto3_AliasedEnum_descriptor.by_name -- Enum: protobuf_test_messages.proto3.EnumOnlyProto3.Bool @@ -72,8 +75,8 @@ M.TestAllTypesProto3_descriptor.fields = { {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="optional_string_piece", id=24, kind='scalar', proto_type="string", options={ctype = "STRING_PIECE"}}, + {name="optional_cord", id=25, kind='scalar', proto_type="string", options={ctype = "CORD"}}, {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}, @@ -94,36 +97,36 @@ M.TestAllTypesProto3_descriptor.fields = { {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="repeated_string_piece", id=54, kind='scalar', proto_type="string", repeated=true, options={ctype = "STRING_PIECE"}}, + {name="repeated_cord", id=55, kind='scalar', proto_type="string", repeated=true, options={ctype = "CORD"}}, + {name="packed_int32", id=75, kind='scalar', proto_type="int32", repeated=true, packed=true, options={packed = true}}, + {name="packed_int64", id=76, kind='scalar', proto_type="int64", repeated=true, packed=true, options={packed = true}}, + {name="packed_uint32", id=77, kind='scalar', proto_type="uint32", repeated=true, packed=true, options={packed = true}}, + {name="packed_uint64", id=78, kind='scalar', proto_type="uint64", repeated=true, packed=true, options={packed = true}}, + {name="packed_sint32", id=79, kind='scalar', proto_type="sint32", repeated=true, packed=true, options={packed = true}}, + {name="packed_sint64", id=80, kind='scalar', proto_type="sint64", repeated=true, packed=true, options={packed = true}}, + {name="packed_fixed32", id=81, kind='scalar', proto_type="fixed32", repeated=true, packed=true, options={packed = true}}, + {name="packed_fixed64", id=82, kind='scalar', proto_type="fixed64", repeated=true, packed=true, options={packed = true}}, + {name="packed_sfixed32", id=83, kind='scalar', proto_type="sfixed32", repeated=true, packed=true, options={packed = true}}, + {name="packed_sfixed64", id=84, kind='scalar', proto_type="sfixed64", repeated=true, packed=true, options={packed = true}}, + {name="packed_float", id=85, kind='scalar', proto_type="float", repeated=true, packed=true, options={packed = true}}, + {name="packed_double", id=86, kind='scalar', proto_type="double", repeated=true, packed=true, options={packed = true}}, + {name="packed_bool", id=87, kind='scalar', proto_type="bool", repeated=true, packed=true, options={packed = true}}, + {name="packed_nested_enum", id=88, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=true, options={packed = true}}, + {name="unpacked_int32", id=89, kind='scalar', proto_type="int32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_int64", id=90, kind='scalar', proto_type="int64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_uint32", id=91, kind='scalar', proto_type="uint32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_uint64", id=92, kind='scalar', proto_type="uint64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sint32", id=93, kind='scalar', proto_type="sint32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sint64", id=94, kind='scalar', proto_type="sint64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_fixed32", id=95, kind='scalar', proto_type="fixed32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_fixed64", id=96, kind='scalar', proto_type="fixed64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sfixed32", id=97, kind='scalar', proto_type="sfixed32", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_sfixed64", id=98, kind='scalar', proto_type="sfixed64", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_float", id=99, kind='scalar', proto_type="float", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_double", id=100, kind='scalar', proto_type="double", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_bool", id=101, kind='scalar', proto_type="bool", repeated=true, packed=false, options={packed = false}}, + {name="unpacked_nested_enum", id=102, kind='enum', enum=M.TestAllTypesProto3_NestedEnum_descriptor, repeated=true, packed=false, options={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"}}, diff --git a/examples/expected/runtime/quickstart/quickstart_pb.lua b/examples/expected/runtime/quickstart/quickstart_pb.lua index f9c91070cdec615b9c07b23528a992acb1572588..93e2f0a393758d2d58d37ad816994ec27c51f69c 100644 --- a/examples/expected/runtime/quickstart/quickstart_pb.lua +++ b/examples/expected/runtime/quickstart/quickstart_pb.lua @@ -8,6 +8,8 @@ local wire = pb.wire local M = {} +M.options = {go_package = "tarantoolpb_synthetic/quickstart"} + -- Enum: quickstart.Role M.Role_descriptor = pb.enum("quickstart.Role", { ROLE_UNSPECIFIED = 0, diff --git a/test/codegen_options_test.lua b/test/codegen_options_test.lua new file mode 100644 index 0000000000000000000000000000000000000000..dd095416712992064a2c913f9dbc7ce71e63885a --- /dev/null +++ b/test/codegen_options_test.lua @@ -0,0 +1,235 @@ +-- Regression test for descriptor-option preservation. +-- +-- protoc-gen-tarantool used to discard everything in *Options messages. +-- The plugin now walks every populated standard field and extension and +-- surfaces them on the generated descriptor as a plain Lua table named +-- `options`, keyed by proto field name for standard options and by +-- fully-qualified extension name (in bracket-string form) for +-- extensions. Empty options remain absent (the `options` key is not +-- emitted at all), so option-free protos produce byte-identical output +-- to before. +-- +-- Coverage: +-- * Message-level options (extension `(opttest_msg_tags)`, repeated). +-- * Field-level options (standard `deprecated`, scalar extensions). +-- * Method-level options (standard `deprecated`, scalar extension, +-- message-valued extension recursing into a oneof + repeated +-- nested-message sub-field — the shape google.api.http uses, the +-- primary real-world consumer of this feature). +-- * Service-level options are exercised on the side too. +-- * Determinism: standard fields sort alphabetically before +-- extensions (which sort alphabetically by full name) — the encoded +-- extension keys land in a predictable order regardless of how +-- protoc ranges them internally. + +local t = require('luatest') +local fio = require('fio') + +local g = t.group('codegen_options') + +local REPO_ROOT = fio.abspath(fio.pathjoin( + fio.dirname(debug.getinfo(1, 'S').source:sub(2)), '..')) +local OPTIONS_DIR = fio.pathjoin(REPO_ROOT, 'options') +local PLUGIN = fio.pathjoin(REPO_ROOT, 'protoc-gen-tarantool') + +local PROTO_BODY = [[ +syntax = "proto3"; +package opttest; +import "google/protobuf/descriptor.proto"; + +// Message-typed extension mirroring google.api.HttpRule's shape — a +// `pattern` oneof, a `body` scalar, and a repeated self-reference for +// additional bindings. Exercises the recursive option emitter on +// nested-message + oneof + repeated-of-message in one shot, without +// pulling in googleapis. +message HttpRule { + oneof pattern { + string get = 1; + string post = 2; + } + string body = 3; + repeated HttpRule additional_bindings = 4; +} + +extend google.protobuf.FieldOptions { + string opttest_field_doc = 50000; + bool opttest_field_secret = 50001; +} +extend google.protobuf.MessageOptions { + repeated string opttest_msg_tags = 50002; +} +extend google.protobuf.ServiceOptions { + string opttest_svc_owner = 50005; +} +extend google.protobuf.MethodOptions { + int32 opttest_timeout_ms = 50003; + HttpRule opttest_http = 50004; +} + +message Req { + option (opttest_msg_tags) = "alpha"; + option (opttest_msg_tags) = "beta"; + + string id = 1 [ + (opttest_field_doc) = "primary id", + (opttest_field_secret) = true, + deprecated = true + ]; + string note = 2; +} +message Resp { string out = 1; } + +service Demo { + option (opttest_svc_owner) = "platform"; + + rpc Plain(Req) returns (Resp); + rpc Annotated(Req) returns (Resp) { + option deprecated = true; + option (opttest_timeout_ms) = 5000; + option (opttest_http) = { + post: "/v1/demo" + body: "*" + additional_bindings { post: "/v2/demo" body: "*" } + }; + } +} +]] + +local function spit(path, content) + local f = assert(io.open(path, 'wb')) + f:write(content) + f:close() +end + +local function ensure_plugin() + if fio.path.exists(PLUGIN) then return end + local cmd = string.format('cd %q && go build -o %s ./cmd/protoc-gen-tarantool', + REPO_ROOT, fio.basename(PLUGIN)) + assert(os.execute(cmd) == 0 or os.execute(cmd) == true, + 'failed to build plugin: ' .. cmd) +end + +local function run_plugin(mode) + local tmp = fio.tempdir() + local proto_dir = fio.pathjoin(tmp, 'proto') + local out_dir = fio.pathjoin(tmp, 'out') + assert(fio.mkdir(proto_dir)) + assert(fio.mkdir(out_dir)) + spit(fio.pathjoin(proto_dir, 'opttest.proto'), PROTO_BODY) + local cmd = string.format( + 'protoc --plugin=%q --tarantool_out=%q ' + ..'--tarantool_opt=mode=%s,prefix=opt_%s ' + ..'-I %q -I %q %q', + PLUGIN, out_dir, mode, mode, proto_dir, OPTIONS_DIR, + fio.pathjoin(proto_dir, 'opttest.proto')) + local ok = os.execute(cmd) + assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd) + return out_dir, ('opt_%s.opttest.opttest_pb'):format(mode) +end + +local function load_module(out, modname) + package.path = fio.pathjoin(out, '?.lua') .. ';' + .. fio.pathjoin(out, '?/init.lua') .. ';' .. package.path + package.loaded[modname] = nil + return require(modname) +end + +g.before_all(function() + ensure_plugin() +end) + +for _, mode in ipairs({'full', 'runtime'}) do + g['test_message_options_'..mode] = function() + local out, modname = run_plugin(mode) + local mod = load_module(out, modname) + + local opts = mod.Req_descriptor.options + t.assert_type(opts, 'table', + 'Req should carry options from (opttest_msg_tags)') + local tags = opts['opttest.opttest_msg_tags'] + t.assert_equals(tags, {'alpha', 'beta'}, + 'repeated extension preserves declaration order') + + -- Option-free message must NOT carry an options key — keeps + -- generated output byte-identical for the common case. + t.assert_equals(mod.Resp_descriptor.options, nil, + 'Resp has no options; the key must be absent') + end + + g['test_field_options_'..mode] = function() + local out, modname = run_plugin(mode) + local mod = load_module(out, modname) + + -- Field id 1 carries deprecated + two custom extensions; field 2 has nothing. + local id_field = mod.Req_descriptor.field_by_name.id + local note_field = mod.Req_descriptor.field_by_name.note + + t.assert_type(id_field.options, 'table', + 'id field should carry options') + t.assert_equals(id_field.options.deprecated, true, + 'standard option `deprecated` surfaces as bare key') + t.assert_equals(id_field.options['opttest.opttest_field_doc'], 'primary id', + 'string extension preserved verbatim') + t.assert_equals(id_field.options['opttest.opttest_field_secret'], true, + 'bool extension preserved') + + t.assert_equals(note_field.options, nil, + 'note has no options; the key must be absent') + end + + g['test_method_options_'..mode] = function() + local out, modname = run_plugin(mode) + local mod = load_module(out, modname) + + -- Plain has no options block; Annotated carries three. + t.assert_equals(mod.Demo_service.methods.Plain.options, nil, + 'Plain has no method options') + + local annotated = mod.Demo_service.methods.Annotated + t.assert_type(annotated.options, 'table', + 'Annotated should carry method options') + t.assert_equals(annotated.options.deprecated, true) + t.assert_equals(annotated.options['opttest.opttest_timeout_ms'], 5000) + + local http = annotated.options['opttest.opttest_http'] + t.assert_type(http, 'table', + 'message-typed extension recurses into a nested Lua table') + t.assert_equals(http.post, '/v1/demo', + 'oneof branch surfaces as a normal key (last-set wins)') + t.assert_equals(http.body, '*') + t.assert_type(http.additional_bindings, 'table') + t.assert_equals(#http.additional_bindings, 1, + 'repeated nested-message extension yields a Lua array') + t.assert_equals(http.additional_bindings[1].post, '/v2/demo') + t.assert_equals(http.additional_bindings[1].body, '*') + end + + g['test_service_options_'..mode] = function() + local out, modname = run_plugin(mode) + local mod = load_module(out, modname) + + t.assert_type(mod.Demo_service.options, 'table') + t.assert_equals(mod.Demo_service.options['opttest.opttest_svc_owner'], + 'platform') + end + + g['test_deterministic_key_order_'..mode] = function() + -- The walker sorts standard fields alphabetically then extensions + -- alphabetically by full name. Re-run the plugin twice and assert + -- the raw file bytes match — independent of protoc's internal + -- field-range order. + local out1 = run_plugin(mode) + local out2 = run_plugin(mode) + local function slurp(path) + local f = assert(io.open(path, 'rb')) + local s = f:read('*a') + f:close() + return s + end + local rel = fio.pathjoin(('opt_%s'):format(mode), + 'opttest', 'opttest_pb.lua') + t.assert_equals(slurp(fio.pathjoin(out1, rel)), + slurp(fio.pathjoin(out2, rel)), + 'codegen output must be byte-identical across runs') + end +end