M PLAN.md => PLAN.md +13 -0
@@ 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
M README.md => README.md +38 -0
@@ 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.<Type>_descriptor.options` — `MessageOptions` (+ extensions)
+- field's `options` (inline) — `FieldOptions` (+ extensions)
+- `M.<Type>_descriptor.oneof_options` — `{oneof_name = OneofOptions}`
+- `M.<Enum>_descriptor.options` — `EnumOptions`
+- `M.<Enum>_descriptor.value_options` — `{VALUE_NAME = EnumValueOptions}`
+- `M.<Svc>_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
A cmd/protoc-gen-tarantool/internal/gen/descopts.go => cmd/protoc-gen-tarantool/internal/gen/descopts.go +344 -0
@@ 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()
+}
M cmd/protoc-gen-tarantool/internal/gen/gen.go => cmd/protoc-gen-tarantool/internal/gen/gen.go +90 -3
@@ 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.<Enum>_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.<Name>_descriptor.oneof_options = { <oname> = {...} }`
+// 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.<Name>_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, ", ") + "}"
}
M cmd/protoc-gen-tarantool/internal/gen/service.go => cmd/protoc-gen-tarantool/internal/gen/service.go +6 -0
@@ 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("")
M examples/expected/full/conformance/conformance_pb.lua => examples/expected/full/conformance/conformance_pb.lua +2 -0
@@ 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,
M examples/expected/full/hello/hello_pb.lua => examples/expected/full/hello/hello_pb.lua +2 -0
@@ 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,
M examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/full/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +35 -32
@@ 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"}},
M examples/expected/full/quickstart/quickstart_pb.lua => examples/expected/full/quickstart/quickstart_pb.lua +2 -0
@@ 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,
M examples/expected/runtime/conformance/conformance_pb.lua => examples/expected/runtime/conformance/conformance_pb.lua +2 -0
@@ 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,
M examples/expected/runtime/hello/hello_pb.lua => examples/expected/runtime/hello/hello_pb.lua +2 -0
@@ 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,
M examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua => examples/expected/runtime/protobuf_test_messages/proto3/test_messages_proto3_pb.lua +35 -32
@@ 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"}},
M examples/expected/runtime/quickstart/quickstart_pb.lua => examples/expected/runtime/quickstart/quickstart_pb.lua +2 -0
@@ 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,
A test/codegen_options_test.lua => test/codegen_options_test.lua +235 -0
@@ 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