~bigbes/tarantool

tarantool-protobuf

06b2978a0f71c67829bbb516ecec6edf32f78667 — Eugene Blikh 3 months ago 76140b7
codegen: resolve (tarantool.lua_package) via global type registry

The custom file option was being dropped silently: protoc encoded it
correctly into the FileDescriptorProto, but protobuf-go parked the
unknown extension in the message's unknown-fields tail because
E_LuaPackage was never registered with protoregistry.GlobalTypes. As a
result proto.GetExtension returned "" and every override the README
documented was a no-op — every caller fell through to the default
"<pkg>.<file>_pb" path resolution.

Register E_LuaPackage in init() and add a luatest regression that
asserts both the output path and the cross-file require strings honor
the option, parameterized over both codegen modes.
2 files changed, 119 insertions(+), 0 deletions(-)

M cmd/protoc-gen-tarantool/internal/gen/options.go
A test/codegen_lua_package_test.lua
M cmd/protoc-gen-tarantool/internal/gen/options.go => cmd/protoc-gen-tarantool/internal/gen/options.go +11 -0
@@ 3,6 3,7 @@ package gen
import (
	"google.golang.org/protobuf/proto"
	"google.golang.org/protobuf/reflect/protoreflect"
	"google.golang.org/protobuf/reflect/protoregistry"
	"google.golang.org/protobuf/runtime/protoimpl"
	"google.golang.org/protobuf/types/descriptorpb"
)


@@ 19,6 20,16 @@ var E_LuaPackage = &protoimpl.ExtensionInfo{
	Filename:      "tarantool/tarantool.proto",
}

// Register the extension in the global type registry so that
// proto.GetExtension can resolve it on FileOptions parsed by protoc-gen-go's
// plugin scaffolding. Without this, the option's value lands in the message's
// unknown fields and GetExtension returns the zero value ("").
func init() {
	if err := protoregistry.GlobalTypes.RegisterExtension(E_LuaPackage); err != nil {
		panic("tarantool-protobuf: register E_LuaPackage: " + err.Error())
	}
}

// luaPackageOption returns the value of (tarantool.lua_package) on the file
// options, or "" when unset.
func luaPackageOption(f protoreflect.FileDescriptor) string {

A test/codegen_lua_package_test.lua => test/codegen_lua_package_test.lua +108 -0
@@ 0,0 1,108 @@
-- Regression test for (tarantool.lua_package) file option.
--
-- The option lives on FileOptions as extension field 60001 and overrides
-- the default `<pkg>.<file>_pb` Lua require path. The plugin parses the
-- extension via protoreflect/protoregistry, so this test guards against
-- the option silently becoming a no-op when the extension is not
-- registered in the global type registry (the failure mode is that
-- proto.GetExtension returns "" and the override is lost).
--
-- Coverage:
--   * The on-disk output path follows `lua_package`, not `<pkg>/<file>_pb`.
--   * Cross-file `import "other.proto"` rewrites to `require("<lua_package>")`.

local t = require('luatest')
local fio = require('fio')

local g = t.group('codegen_lua_package')

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 function spit(path, content)
    local f = assert(io.open(path, 'wb'))
    f:write(content)
    f:close()
end

local function slurp(path)
    local f = assert(io.open(path, 'rb'))
    local s = f:read('*a')
    f:close()
    return s
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 SAMPLE_PROTO = [[
syntax = "proto3";
package sample;
import "tarantool/tarantool.proto";
option (tarantool.lua_package) = "myapp.proto.sample";
message Foo { int32 x = 1; }
]]

local OTHER_PROTO = [[
syntax = "proto3";
package other;
import "tarantool/tarantool.proto";
import "sample.proto";
option (tarantool.lua_package) = "myapp.proto.other";
message Bar { sample.Foo foo = 1; }
]]

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, 'sample.proto'), SAMPLE_PROTO)
    spit(fio.pathjoin(proto_dir, 'other.proto'), OTHER_PROTO)

    local cmd = string.format(
        'protoc --plugin=%q --tarantool_out=%q --tarantool_opt=mode=%s '
        ..'-I %q -I %q %q %q',
        PLUGIN, out_dir, mode, proto_dir, OPTIONS_DIR,
        fio.pathjoin(proto_dir, 'sample.proto'),
        fio.pathjoin(proto_dir, 'other.proto'))
    local ok = os.execute(cmd)
    assert(ok == 0 or ok == true, 'plugin failed: ' .. cmd)
    return out_dir
end

g.before_all(function()
    ensure_plugin()
end)

for _, mode in ipairs({'full', 'runtime'}) do
    g['test_output_path_honors_lua_package_'..mode] = function()
        local out = run_plugin(mode)
        t.assert(fio.path.exists(fio.pathjoin(out, 'myapp', 'proto', 'sample.lua')),
                 'expected sample.lua under lua_package path, got: '
                 .. table.concat(fio.glob(fio.pathjoin(out, '**', '*.lua')) or {}, ', '))
        t.assert(fio.path.exists(fio.pathjoin(out, 'myapp', 'proto', 'other.lua')),
                 'expected other.lua under lua_package path')
        -- Default `<pkg>/<file>_pb.lua` layout must NOT be present —
        -- that's the regression signature (extension dropped, fallback used).
        t.assert_not(fio.path.exists(fio.pathjoin(out, 'sample', 'sample_pb.lua')),
                     'fallback path was emitted — lua_package option was ignored')
    end

    g['test_cross_file_require_honors_lua_package_'..mode] = function()
        local out = run_plugin(mode)
        local body = slurp(fio.pathjoin(out, 'myapp', 'proto', 'other.lua'))
        t.assert_str_contains(body, 'require("myapp.proto.sample")', false,
            'cross-file import should resolve via lua_package, not the default path')
        t.assert_not(string.find(body, 'require("sample.sample_pb")', 1, true),
                     'default require path leaked into cross-file import')
    end
end