~bigbes/tarantool

tarantool-protobuf

7676fdc2fdd14066e363732e504a8f640a01cab3 — Eugene Blikh 3 months ago 7ae7231
codegen: preserve descriptor options as `options = { ... }`

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.
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