From 1b5b7e8b260cae076b59c06532505e5937c07caa Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 16 Jun 2026 16:32:29 +0800 Subject: [PATCH 1/4] refactor: extract shared codegen helpers into internal/genhelper and dedup cpp/csharp/go loaders - add internal/genhelper with shared file-header (ProtocVersion/header) and map-key (MapKey/MapKeySlice) helpers - replace per-loader duplicated helper code in cpp/csharp/go by reusing the shared genhelper package - internal/options: extract GetWorksheetOptions/IsWorksheet shared helpers - csharp: drop unused gen/messagerName params from genMessage/genMapGetters/generateFileContent - remove obsolete cmd/protoc-gen-go-tableau-loader/helper.go - test(csharp): align shared fixtures (HubFixture/LoadTests) --- .../helper/helper.go | 109 ++---------------- cmd/protoc-gen-cpp-tableau-loader/messager.go | 12 +- .../helper/helper.go | 104 ++--------------- .../messager.go | 22 ++-- cmd/protoc-gen-go-tableau-loader/embed.go | 3 +- cmd/protoc-gen-go-tableau-loader/helper.go | 35 ------ .../helper/helper.go | 98 ++++------------ cmd/protoc-gen-go-tableau-loader/messager.go | 10 +- internal/genhelper/header.go | 37 ++++++ internal/genhelper/mapkey.go | 95 +++++++++++++++ internal/index/index.go | 7 +- internal/options/options.go | 27 +++-- internal/xproto/protofile.go | 11 +- .../csharp-tableau-loader/tests/HubFixture.cs | 9 +- test/csharp-tableau-loader/tests/LoadTests.cs | 20 +++- 15 files changed, 234 insertions(+), 365 deletions(-) delete mode 100644 cmd/protoc-gen-go-tableau-loader/helper.go create mode 100644 internal/genhelper/header.go create mode 100644 internal/genhelper/mapkey.go diff --git a/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go b/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go index 2f832048..9e515ebd 100644 --- a/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/iancoleman/strcase" + "github.com/tableauio/loader/internal/genhelper" "github.com/tableauio/tableau/proto/tableaupb" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" @@ -14,33 +15,17 @@ import ( func GenerateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, version string) { GenerateCommonHeader(gen, g, version) - if file.Proto.GetOptions().GetDeprecated() { - g.P("// ", file.Desc.Path(), " is a deprecated file.") - } else { - g.P("// source: ", file.Desc.Path()) - } + genhelper.GenerateSourcePath(file, g) } func GenerateCommonHeader(gen *protogen.Plugin, g *protogen.GeneratedFile, version string) { g.P("// Code generated by protoc-gen-cpp-tableau-loader. DO NOT EDIT.") g.P("// versions:") g.P("// - protoc-gen-cpp-tableau-loader v", version) - g.P("// - protoc ", protocVersion(gen)) + g.P("// - protoc ", genhelper.ProtocVersion(gen)) g.P("// clang-format off") } -func protocVersion(gen *protogen.Plugin) string { - v := gen.Request.GetCompilerVersion() - if v == nil { - return "(unknown)" - } - var suffix string - if s := v.GetSuffix(); s != "" { - suffix = "-" + s - } - return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix) -} - func ParseCppFieldName(fd protoreflect.FieldDescriptor) string { return escapeIdentifier(string(fd.Name())) } @@ -174,102 +159,30 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect return mapFd.MapValue().Kind().String() } -type MapKey struct { - Type string - Name string - FieldName string // multi-column index only (may be deduplicated, e.g., "Id" → "Id3") - OrigFieldName string // original FieldName before deduplication (empty if not renamed) - Fd protoreflect.FieldDescriptor // the map field descriptor this key belongs to -} +// MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. +type MapKey = genhelper.MapKey type MapKeySlice []MapKey -// AddMapKey appends a new map key to the slice, automatically deduplicating -// both Name (used as function parameter names) and FieldName (used as struct -// field names in LevelIndex key structs). -// -// Deduplication is needed because different map levels may share the same key -// name. For example, given the following nested proto maps where country_map -// and item_map both use "ID" as their key name: -// -// message Fruit4Conf { -// map fruit_map = 1; // key field: "FruitType" -// message Fruit { -// map country_map = 2; // key field: "ID" -// message Country { -// map item_map = 3; // key field: "ID" ← same name! -// } -// } -// } -// -// Without dedup, the generated LevelIndex key struct would have duplicate -// field names, causing a compile error: -// -// struct LevelIndex_Fruit_Country_ItemKey { -// int32_t id; // key of protoconf.Fruit4Conf.fruit_map -// int32_t id; // key of protoconf.Fruit4Conf.Fruit.country_map -// int32_t id; // key of protoconf.Fruit4Conf.Fruit.Country.item_map — COMPILE ERROR! -// }; -// -// With dedup, the conflicting name gets a numeric suffix (the 1-based position -// of the new key in the slice), producing valid C++ code: -// -// struct LevelIndex_Fruit_Country_ItemKey { -// int32_t fruit_type; // key of protoconf.Fruit4Conf.fruit_map -// int32_t id; // key of protoconf.Fruit4Conf.Fruit.country_map -// int32_t id3; // key of protoconf.Fruit4Conf.Fruit.Country.item_map (renamed from id) -// }; +// AddMapKey appends a new map key, deduplicating Name and FieldName. +// See genhelper.AddMapKey for the full rationale. func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - if newKey.Name == "" { - newKey.Name = fmt.Sprintf("key%d", len(s)+1) - } - // Deduplicate Name (used as function parameter, e.g., "id" → "id3"). - for _, key := range s { - if key.Name == newKey.Name { - newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1) - break - } - } - // Deduplicate FieldName (used as struct field, e.g., "Id" → "Id3"). - // This is only relevant for multi-column indexes that generate LevelIndex - // key structs; single-column indexes leave FieldName empty. - if newKey.FieldName != "" { - for _, key := range s { - if key.FieldName == newKey.FieldName { - newKey.OrigFieldName = newKey.FieldName - newKey.FieldName = fmt.Sprintf("%s%d", newKey.FieldName, len(s)+1) - break - } - } - } - return append(s, newKey) + return genhelper.AddMapKey(s, newKey) } // GenGetParams generates function parameters, which are the names listed in the function's definition. func (s MapKeySlice) GenGetParams() string { - var params []string - for _, key := range s { - params = append(params, ToConstRefType(key.Type)+" "+key.Name) - } - return strings.Join(params, ", ") + return genhelper.GenCustom(s, func(key MapKey) string { return ToConstRefType(key.Type) + " " + key.Name }, ", ") } // GenGetArguments generates function arguments, which are the real values passed to the function. func (s MapKeySlice) GenGetArguments() string { - var params []string - for _, key := range s { - params = append(params, key.Name) - } - return strings.Join(params, ", ") + return genhelper.GenGetArguments(s) } // GenOtherArguments generates function arguments for other value of std::tie. func (s MapKeySlice) GenOtherArguments(other string) string { - var params []string - for _, key := range s { - params = append(params, other+"."+key.Name) - } - return strings.Join(params, ", ") + return genhelper.GenCustom(s, func(key MapKey) string { return other + "." + key.Name }, ", ") } func Indent(depth int) string { diff --git a/cmd/protoc-gen-cpp-tableau-loader/messager.go b/cmd/protoc-gen-cpp-tableau-loader/messager.go index 9bea0627..5983d803 100644 --- a/cmd/protoc-gen-cpp-tableau-loader/messager.go +++ b/cmd/protoc-gen-cpp-tableau-loader/messager.go @@ -8,11 +8,9 @@ import ( "github.com/tableauio/loader/cmd/protoc-gen-cpp-tableau-loader/orderedmap" "github.com/tableauio/loader/internal/extensions" "github.com/tableauio/loader/internal/index" - "github.com/tableauio/tableau/proto/tableaupb" + "github.com/tableauio/loader/internal/options" "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) // generateMessager generates protobuf message wrapped classes @@ -56,9 +54,7 @@ func generateHppFileContent(file *protogen.File, g *protogen.GeneratedFile) { g.P("namespace ", *namespace, " {") var fileMessagers []string for _, message := range file.Messages { - opts := message.Desc.Options().(*descriptorpb.MessageOptions) - worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if worksheet != nil { + if options.IsWorksheet(message.Desc) { genHppMessage(g, message) messagerName := string(message.Desc.Name()) fileMessagers = append(fileMessagers, messagerName) @@ -142,9 +138,7 @@ func generateCppFileContent(file *protogen.File, g *protogen.GeneratedFile) { g.P("namespace ", *namespace, " {") for _, message := range file.Messages { - opts := message.Desc.Options().(*descriptorpb.MessageOptions) - worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if worksheet != nil { + if options.IsWorksheet(message.Desc) { genCppMessage(g, message) } } diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go index 70921485..b983fef0 100644 --- a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go @@ -6,6 +6,7 @@ import ( "unicode" "github.com/iancoleman/strcase" + "github.com/tableauio/loader/internal/genhelper" "github.com/tableauio/tableau/proto/tableaupb" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" @@ -21,32 +22,12 @@ func GenerateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.G g.P("// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT.") g.P("// versions:") g.P("// - protoc-gen-csharp-tableau-loader v", version) - g.P("// - protoc ", protocVersion(gen)) - if file != nil { - if file.Proto.GetOptions().GetDeprecated() { - g.P("// ", file.Desc.Path(), " is a deprecated file.") - } else { - g.P("// source: ", file.Desc.Path()) - } - } + g.P("// - protoc ", genhelper.ProtocVersion(gen)) + genhelper.GenerateSourcePath(file, g) g.P("// ") g.P("#nullable enable") } -// protocVersion returns the protoc compiler version string (e.g. "v3.19.3") -// extracted from the code generator request. Returns "(unknown)" if not available. -func protocVersion(gen *protogen.Plugin) string { - v := gen.Request.GetCompilerVersion() - if v == nil { - return "(unknown)" - } - var suffix string - if s := v.GetSuffix(); s != "" { - suffix = "-" + s - } - return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix) -} - // ParseIndexFieldName returns the C# property name for an index field descriptor. // It delegates to ParseCsharpPropertyName to match protoc's C# naming convention. func ParseIndexFieldName(fd protoreflect.FieldDescriptor) string { @@ -321,79 +302,18 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect return mapFd.MapValue().Kind().String() } -// MapKey represents a single key component in a map or composite index, -// holding the C# type, parameter name, original field name, and the -// associated protobuf field descriptor. -type MapKey struct { - Type string // C# type string (e.g. "int", "string") - Name string // parameter/variable name in generated code - FieldName string // multi-column index only (may be deduplicated, e.g., "Id" → "Id3") - OrigFieldName string // original FieldName before deduplication (empty if not renamed) - Fd protoreflect.FieldDescriptor // the map field descriptor this key belongs to -} +// MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. +type MapKey = genhelper.MapKey // MapKeySlice is an ordered collection of MapKey entries, providing methods // to build function parameters, arguments, and custom formatted strings // for code generation. type MapKeySlice []MapKey -// AddMapKey appends a new map key to the slice, automatically deduplicating -// both Name (used as function parameter names) and FieldName (used as struct -// field names in LevelIndex key structs). -// -// Deduplication is needed because different map levels may share the same key -// name. For example, given the following nested proto maps where country_map -// and item_map both use "ID" as their key name: -// -// message Fruit4Conf { -// map fruit_map = 1; // key field: "FruitType" -// message Fruit { -// map country_map = 2; // key field: "ID" -// message Country { -// map item_map = 3; // key field: "ID" ← same name! -// } -// } -// } -// -// Without dedup, the generated LevelIndex key struct would have duplicate -// field names, causing a compile error: -// -// public readonly struct LevelIndex_Fruit_Country_ItemKey { -// public int Id { get; } // key of protoconf.Fruit4Conf.Fruit.country_map -// public int Id { get; } // key of protoconf.Fruit4Conf.Fruit.Country.item_map — COMPILE ERROR! -// } -// -// With dedup, the conflicting name gets a numeric suffix (the 1-based position -// of the new key in the slice), producing valid C# code: -// -// public readonly struct LevelIndex_Fruit_Country_ItemKey { -// public int Id { get; } // key of protoconf.Fruit4Conf.Fruit.country_map -// public int Id3 { get; } // key of protoconf.Fruit4Conf.Fruit.Country.item_map (renamed from Id) -// } +// AddMapKey appends a new map key, deduplicating Name and FieldName. +// See genhelper.AddMapKey for the full rationale. func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - if newKey.Name == "" { - newKey.Name = fmt.Sprintf("key%d", len(s)+1) - } - // Deduplicate Name (used as function parameter, e.g., "id" → "id3"). - for _, key := range s { - if key.Name == newKey.Name { - newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1) - break - } - } - // Deduplicate FieldName (used as struct field, e.g., "Id" → "Id3"). - // This is only relevant for multi-column indexes that generate LevelIndex - // key structs; single-column indexes leave FieldName empty. - if newKey.FieldName != "" { - for _, key := range s { - if key.FieldName == newKey.FieldName { - newKey.OrigFieldName = newKey.FieldName - newKey.FieldName = fmt.Sprintf("%s%d", newKey.FieldName, len(s)+1) - break - } - } - } - return append(s, newKey) + return genhelper.AddMapKey(s, newKey) } // GenGetParams generates function parameters, which are the names listed in the function's definition. @@ -403,17 +323,13 @@ func (s MapKeySlice) GenGetParams() string { // GenGetArguments generates function arguments, which are the real values passed to the function. func (s MapKeySlice) GenGetArguments() string { - return s.GenCustom(func(key MapKey) string { return key.Name }, ", ") + return genhelper.GenGetArguments(s) } // GenCustom generates a string by applying fn to each MapKey and joining // the results with the given separator. Returns an empty string for empty slices. func (s MapKeySlice) GenCustom(fn func(MapKey) string, sep string) string { - var params []string - for _, key := range s { - params = append(params, fn(key)) - } - return strings.Join(params, sep) + return genhelper.GenCustom(s, fn, sep) } // Indent returns a string of 4*depth spaces, used for indenting generated diff --git a/cmd/protoc-gen-csharp-tableau-loader/messager.go b/cmd/protoc-gen-csharp-tableau-loader/messager.go index 2a5552de..6567668a 100644 --- a/cmd/protoc-gen-csharp-tableau-loader/messager.go +++ b/cmd/protoc-gen-csharp-tableau-loader/messager.go @@ -11,11 +11,9 @@ import ( "github.com/tableauio/loader/internal/extensions" "github.com/tableauio/loader/internal/index" "github.com/tableauio/loader/internal/loadutil" - "github.com/tableauio/tableau/proto/tableaupb" + "github.com/tableauio/loader/internal/options" "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) // generateMessager generates a protoconf file corresponding to the protobuf file. @@ -25,29 +23,27 @@ func generateMessager(gen *protogen.Plugin, file *protogen.File) { filename := filepath.Join(strcase.ToCamel(file.GeneratedFilenamePrefix) + "." + extensions.PC + ".cs") g := gen.NewGeneratedFile(filename, "") helper.GenerateFileHeader(gen, file, g, version) - generateFileContent(gen, file, g) + generateFileContent(file, g) } // generateFileContent generates struct type definitions. -func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { +func generateFileContent(file *protogen.File, g *protogen.GeneratedFile) { g.P(staticMessagerContent1) firstMessager := true for _, message := range file.Messages { - opts := message.Desc.Options().(*descriptorpb.MessageOptions) - worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if worksheet != nil { + if options.IsWorksheet(message.Desc) { if !firstMessager { g.P() } firstMessager = false - genMessage(gen, g, message) + genMessage(g, message) } } g.P(staticMessagerContent2) } // genMessage generates a message definition. -func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, message *protogen.Message) { +func genMessage(g *protogen.GeneratedFile, message *protogen.Message) { messagerName := string(message.Desc.Name()) indexDescriptor := index.ParseIndexDescriptor(message.Desc) @@ -118,13 +114,13 @@ func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, message *protog } // syntactic sugar for accessing map items - genMapGetters(gen, g, message.Desc, 1, nil, messagerName) + genMapGetters(g, message.Desc, 1, nil) orderedMapGenerator.GenOrderedMapGetters() indexGenerator.GenIndexFinders() g.P(helper.Indent(1), "}") } -func genMapGetters(gen *protogen.Plugin, g *protogen.GeneratedFile, md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice, messagerName string) { +func genMapGetters(g *protogen.GeneratedFile, md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice) { for i := 0; i < md.Fields().Len(); i++ { fd := md.Fields().Get(i) if fd.IsMap() { @@ -151,7 +147,7 @@ func genMapGetters(gen *protogen.Plugin, g *protogen.GeneratedFile, md protorefl } if fd.MapValue().Kind() == protoreflect.MessageKind { - genMapGetters(gen, g, fd.MapValue().Message(), depth+1, keys, messagerName) + genMapGetters(g, fd.MapValue().Message(), depth+1, keys) } break } diff --git a/cmd/protoc-gen-go-tableau-loader/embed.go b/cmd/protoc-gen-go-tableau-loader/embed.go index da83a566..93e67f1c 100644 --- a/cmd/protoc-gen-go-tableau-loader/embed.go +++ b/cmd/protoc-gen-go-tableau-loader/embed.go @@ -6,6 +6,7 @@ import ( "text/template" "github.com/iancoleman/strcase" + "github.com/tableauio/loader/cmd/protoc-gen-go-tableau-loader/helper" "github.com/tableauio/loader/internal/xproto" "google.golang.org/protobuf/compiler/protogen" ) @@ -29,7 +30,7 @@ func generateEmbed(gen *protogen.Plugin) { } g := gen.NewGeneratedFile(strings.TrimSuffix(entry.Name(), ".tpl"), "") - generateCommonHeader(gen, g) + helper.GenerateCommonHeader(gen, g, version) g.P() g.P("package ", *pkg) g.P() diff --git a/cmd/protoc-gen-go-tableau-loader/helper.go b/cmd/protoc-gen-go-tableau-loader/helper.go deleted file mode 100644 index a158013b..00000000 --- a/cmd/protoc-gen-go-tableau-loader/helper.go +++ /dev/null @@ -1,35 +0,0 @@ -package main - -import ( - "fmt" - - "google.golang.org/protobuf/compiler/protogen" -) - -func generateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { - generateCommonHeader(gen, g) - if file.Proto.GetOptions().GetDeprecated() { - g.P("// ", file.Desc.Path(), " is a deprecated file.") - } else { - g.P("// source: ", file.Desc.Path()) - } -} - -func generateCommonHeader(gen *protogen.Plugin, g *protogen.GeneratedFile) { - g.P("// Code generated by protoc-gen-go-tableau-loader. DO NOT EDIT.") - g.P("// versions:") - g.P("// - protoc-gen-go-tableau-loader v", version) - g.P("// - protoc ", protocVersion(gen)) -} - -func protocVersion(gen *protogen.Plugin) string { - v := gen.Request.GetCompilerVersion() - if v == nil { - return "(unknown)" - } - var suffix string - if s := v.GetSuffix(); s != "" { - suffix = "-" + s - } - return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix) -} diff --git a/cmd/protoc-gen-go-tableau-loader/helper/helper.go b/cmd/protoc-gen-go-tableau-loader/helper/helper.go index e84d1cef..af588e17 100644 --- a/cmd/protoc-gen-go-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-go-tableau-loader/helper/helper.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/iancoleman/strcase" + "github.com/tableauio/loader/internal/genhelper" "github.com/tableauio/tableau/proto/tableaupb" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" @@ -12,6 +13,21 @@ import ( "google.golang.org/protobuf/types/descriptorpb" ) +// GenerateFileHeader writes the auto-generated file header comment block, +// including version info and source path. +func GenerateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, version string) { + GenerateCommonHeader(gen, g, version) + genhelper.GenerateSourcePath(file, g) +} + +// GenerateCommonHeader writes the auto-generated common header comment block. +func GenerateCommonHeader(gen *protogen.Plugin, g *protogen.GeneratedFile, version string) { + g.P("// Code generated by protoc-gen-go-tableau-loader. DO NOT EDIT.") + g.P("// versions:") + g.P("// - protoc-gen-go-tableau-loader v", version) + g.P("// - protoc ", genhelper.ProtocVersion(gen)) +} + func ParseIndexFieldName(gen *protogen.Plugin, fd protoreflect.FieldDescriptor) string { md := fd.ContainingMessage() msg := FindMessage(gen, md) @@ -258,91 +274,23 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect return mapFd.MapValue().Kind().String() } -type MapKey struct { - Type string - Name string - FieldName string // multi-column index only (may be deduplicated, e.g., "Id" → "Id3") - OrigFieldName string // original FieldName before deduplication (empty if not renamed) - Fd protoreflect.FieldDescriptor // the map field descriptor this key belongs to -} +// MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. +type MapKey = genhelper.MapKey type MapKeySlice []MapKey -// AddMapKey appends a new map key to the slice, automatically deduplicating -// both Name (used as function parameter names) and FieldName (used as struct -// field names in LevelIndex key structs). -// -// Deduplication is needed because different map levels may share the same key -// name. For example, given the following nested proto maps where country_map -// and item_map both use "ID" as their key name: -// -// message Fruit4Conf { -// map fruit_map = 1; // key field: "FruitType" -// message Fruit { -// map country_map = 2; // key field: "ID" -// message Country { -// map item_map = 3; // key field: "ID" ← same name! -// } -// } -// } -// -// Without dedup, the generated LevelIndex key struct would have duplicate -// field names, causing a compile error: -// -// type Fruit4Conf_LevelIndex_Fruit_Country_ItemKey struct { -// FruitType int32 // key of protoconf.Fruit4Conf.fruit_map -// Id int32 // key of protoconf.Fruit4Conf.Fruit.country_map -// Id int32 // key of protoconf.Fruit4Conf.Fruit.Country.item_map — COMPILE ERROR! -// } -// -// With dedup, the conflicting name gets a numeric suffix (the 1-based position -// of the new key in the slice), producing valid Go code: -// -// type Fruit4Conf_LevelIndex_Fruit_Country_ItemKey struct { -// FruitType int32 // key of protoconf.Fruit4Conf.fruit_map -// Id int32 // key of protoconf.Fruit4Conf.Fruit.country_map -// Id3 int32 // key of protoconf.Fruit4Conf.Fruit.Country.item_map (renamed from Id) -// } +// AddMapKey appends a new map key, deduplicating Name and FieldName. +// See genhelper.AddMapKey for the full rationale. func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - if newKey.Name == "" { - newKey.Name = fmt.Sprintf("key%d", len(s)+1) - } - // Deduplicate Name (used as function parameter, e.g., "id" → "id3"). - for _, key := range s { - if key.Name == newKey.Name { - newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1) - break - } - } - // Deduplicate FieldName (used as struct field, e.g., "Id" → "Id3"). - // This is only relevant for multi-column indexes that generate LevelIndex - // key structs; single-column indexes leave FieldName empty. - if newKey.FieldName != "" { - for _, key := range s { - if key.FieldName == newKey.FieldName { - newKey.OrigFieldName = newKey.FieldName - newKey.FieldName = fmt.Sprintf("%s%d", newKey.FieldName, len(s)+1) - break - } - } - } - return append(s, newKey) + return genhelper.AddMapKey(s, newKey) } // GenGetParams generates function parameters, which are the names listed in the function's definition. func (s MapKeySlice) GenGetParams() string { - var params []string - for _, key := range s { - params = append(params, key.Name+" "+key.Type) - } - return strings.Join(params, ", ") + return genhelper.GenCustom(s, func(key MapKey) string { return key.Name + " " + key.Type }, ", ") } // GenGetArguments generates function arguments, which are the real values passed to the function. func (s MapKeySlice) GenGetArguments() string { - var params []string - for _, key := range s { - params = append(params, key.Name) - } - return strings.Join(params, ", ") + return genhelper.GenGetArguments(s) } diff --git a/cmd/protoc-gen-go-tableau-loader/messager.go b/cmd/protoc-gen-go-tableau-loader/messager.go index 17e8fd85..c30fa4f4 100644 --- a/cmd/protoc-gen-go-tableau-loader/messager.go +++ b/cmd/protoc-gen-go-tableau-loader/messager.go @@ -9,11 +9,9 @@ import ( "github.com/tableauio/loader/internal/extensions" "github.com/tableauio/loader/internal/index" "github.com/tableauio/loader/internal/loadutil" - "github.com/tableauio/tableau/proto/tableaupb" + "github.com/tableauio/loader/internal/options" "google.golang.org/protobuf/compiler/protogen" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) // generateMessager generates a protoconf file corresponding to the protobuf file. @@ -21,7 +19,7 @@ import ( func generateMessager(gen *protogen.Plugin, file *protogen.File) { filename := file.GeneratedFilenamePrefix + "." + extensions.PC + ".go" g := gen.NewGeneratedFile(filename, "") - generateFileHeader(gen, file, g) + helper.GenerateFileHeader(gen, file, g, version) g.P() g.P("package ", *pkg) g.P() @@ -32,9 +30,7 @@ func generateMessager(gen *protogen.Plugin, file *protogen.File) { func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { var fileMessagers []string for _, message := range file.Messages { - opts := message.Desc.Options().(*descriptorpb.MessageOptions) - worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if worksheet != nil { + if options.IsWorksheet(message.Desc) { genMessage(gen, g, message) messagerName := string(message.Desc.Name()) diff --git a/internal/genhelper/header.go b/internal/genhelper/header.go new file mode 100644 index 00000000..1d493909 --- /dev/null +++ b/internal/genhelper/header.go @@ -0,0 +1,37 @@ +// Package genhelper provides cross-language common helpers shared by all +// protoc-gen-*-tableau-loader plugins, such as generated-file header utilities. +package genhelper + +import ( + "fmt" + + "google.golang.org/protobuf/compiler/protogen" +) + +// ProtocVersion returns the protoc compiler version string (e.g. "v3.19.3") +// extracted from the code generator request. Returns "(unknown)" if not +// available. +func ProtocVersion(gen *protogen.Plugin) string { + v := gen.Request.GetCompilerVersion() + if v == nil { + return "(unknown)" + } + var suffix string + if s := v.GetSuffix(); s != "" { + suffix = "-" + s + } + return fmt.Sprintf("v%d.%d.%d%s", v.GetMajor(), v.GetMinor(), v.GetPatch(), suffix) +} + +// GenerateSourcePath writes the source path comment (or a deprecation notice) +// for the given file. It is a no-op if file is nil. +func GenerateSourcePath(file *protogen.File, g *protogen.GeneratedFile) { + if file == nil { + return + } + if file.Proto.GetOptions().GetDeprecated() { + g.P("// ", file.Desc.Path(), " is a deprecated file.") + } else { + g.P("// source: ", file.Desc.Path()) + } +} diff --git a/internal/genhelper/mapkey.go b/internal/genhelper/mapkey.go new file mode 100644 index 00000000..2b1e9bc8 --- /dev/null +++ b/internal/genhelper/mapkey.go @@ -0,0 +1,95 @@ +package genhelper + +import ( + "fmt" + "strings" + + "google.golang.org/protobuf/reflect/protoreflect" +) + +// MapKey represents a single key component of a (possibly nested / composite) +// map getter or index finder. It is shared by all protoc-gen-*-tableau-loader +// plugins: the Go / C++ / C# loaders alias it directly (type MapKey = +// genhelper.MapKey), while the TypeScript loader embeds it to add +// language-specific fields, so every loader's MapKey shares the same base. +type MapKey struct { + // Type is the generated-language type string of the key (e.g. "int32" for + // Go, "int32_t" for C++, "int" for C#, "number" for TypeScript). + Type string + // Name is the parameter/variable name in generated code (deduplicated + // across nested levels, e.g. "id" → "id3"). + Name string + // FieldName is the key struct field name, used by multi-column indexes that + // generate LevelIndex key structs (may be deduplicated, e.g. "Id" → "Id3"). + // Empty for single-column indexes. + FieldName string + // OrigFieldName is the original FieldName before deduplication (empty if not + // renamed). + OrigFieldName string + // Fd is the map field descriptor this key belongs to. + Fd protoreflect.FieldDescriptor +} + +// AddMapKey appends newKey to s, automatically deduplicating both Name (used as +// function parameter names) and FieldName (used as struct field names in +// LevelIndex key structs). +// +// Deduplication is needed because different map levels may share the same key +// name. For example, given the following nested proto maps where country_map +// and item_map both use "ID" as their key name: +// +// message Fruit4Conf { +// map fruit_map = 1; // key field: "FruitType" +// message Fruit { +// map country_map = 2; // key field: "ID" +// message Country { +// map item_map = 3; // key field: "ID" ← same name! +// } +// } +// } +// +// Without dedup, the generated LevelIndex key struct would have duplicate field +// names, causing a compile error. With dedup, the conflicting name gets a +// numeric suffix (the 1-based position of the new key in the slice), producing +// valid code, e.g. "FruitType", "Id", "Id3". +func AddMapKey(s []MapKey, newKey MapKey) []MapKey { + if newKey.Name == "" { + newKey.Name = fmt.Sprintf("key%d", len(s)+1) + } + // Deduplicate Name (used as function parameter, e.g., "id" → "id3"). + for _, key := range s { + if key.Name == newKey.Name { + newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1) + break + } + } + // Deduplicate FieldName (used as struct field, e.g., "Id" → "Id3"). + // This is only relevant for multi-column indexes that generate LevelIndex + // key structs; single-column indexes leave FieldName empty. + if newKey.FieldName != "" { + for _, key := range s { + if key.FieldName == newKey.FieldName { + newKey.OrigFieldName = newKey.FieldName + newKey.FieldName = fmt.Sprintf("%s%d", newKey.FieldName, len(s)+1) + break + } + } + } + return append(s, newKey) +} + +// GenGetArguments generates function arguments, which are the real values +// passed to the function (i.e. the key Names joined by ", "). +func GenGetArguments(s []MapKey) string { + return GenCustom(s, func(key MapKey) string { return key.Name }, ", ") +} + +// GenCustom builds a string by applying fn to each MapKey and joining the +// results with sep. Returns an empty string for an empty slice. +func GenCustom(s []MapKey, fn func(MapKey) string, sep string) string { + var params []string + for _, key := range s { + params = append(params, fn(key)) + } + return strings.Join(params, sep) +} diff --git a/internal/index/index.go b/internal/index/index.go index 36354d25..f783d426 100644 --- a/internal/index/index.go +++ b/internal/index/index.go @@ -4,10 +4,8 @@ import ( "regexp" "strings" - "github.com/tableauio/tableau/proto/tableaupb" - "google.golang.org/protobuf/proto" + "github.com/tableauio/loader/internal/options" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) var indexRegexp *regexp.Regexp @@ -77,8 +75,7 @@ func (index *Index) String() string { // parse worksheet option index func ParseWSOptionIndex(md protoreflect.MessageDescriptor) ([]*Index, []*Index) { - opts := md.Options().(*descriptorpb.MessageOptions) - wsOpts := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) + wsOpts := options.GetWorksheetOptions(md) return parseIndexFrom(wsOpts.Index), parseIndexFrom(wsOpts.OrderedIndex) } diff --git a/internal/options/options.go b/internal/options/options.go index b9046b22..5feda0ca 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -23,11 +23,24 @@ const ( LangCPP Language = "cpp" LangGO Language = "go" LangCS Language = "cs" + LangTS Language = "ts" ) -func NeedGenOrderedMap(md protoreflect.MessageDescriptor, lang Language) bool { +// GetWorksheetOptions returns the worksheet options of the message descriptor. +// It returns nil if the message is not a tableau worksheet. +func GetWorksheetOptions(md protoreflect.MessageDescriptor) *tableaupb.WorksheetOptions { opts := md.Options().(*descriptorpb.MessageOptions) - wsOpts := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) + return proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) +} + +// IsWorksheet reports whether the message is a tableau worksheet, i.e. it has +// the tableaupb.E_Worksheet extension set. +func IsWorksheet(md protoreflect.MessageDescriptor) bool { + return GetWorksheetOptions(md) != nil +} + +func NeedGenOrderedMap(md protoreflect.MessageDescriptor, lang Language) bool { + wsOpts := GetWorksheetOptions(md) if !wsOpts.GetOrderedMap() { // Not an ordered map. return false @@ -42,8 +55,7 @@ func NeedGenOrderedMap(md protoreflect.MessageDescriptor, lang Language) bool { } func NeedGenIndex(md protoreflect.MessageDescriptor, lang Language) bool { - opts := md.Options().(*descriptorpb.MessageOptions) - wsOpts := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) + wsOpts := GetWorksheetOptions(md) if len(wsOpts.GetIndex()) == 0 { // No index. return false @@ -58,8 +70,7 @@ func NeedGenIndex(md protoreflect.MessageDescriptor, lang Language) bool { } func NeedGenOrderedIndex(md protoreflect.MessageDescriptor, lang Language) bool { - opts := md.Options().(*descriptorpb.MessageOptions) - wsOpts := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) + wsOpts := GetWorksheetOptions(md) if len(wsOpts.GetOrderedIndex()) == 0 { // No index. return false @@ -85,9 +96,7 @@ func NeedGenFile(f *protogen.File) bool { } for _, message := range f.Messages { - opts := message.Desc.Options().(*descriptorpb.MessageOptions) - worksheet := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if worksheet != nil { + if IsWorksheet(message.Desc) { return true } } diff --git a/internal/xproto/protofile.go b/internal/xproto/protofile.go index c57a048a..082989d0 100644 --- a/internal/xproto/protofile.go +++ b/internal/xproto/protofile.go @@ -1,7 +1,6 @@ package xproto import ( - "errors" "sort" "github.com/tableauio/loader/internal/options" @@ -36,15 +35,7 @@ func ParseProtoFiles(gen *protogen.Plugin) ProtoFiles { } var messagers []string for _, message := range f.Messages { - opts, ok := message.Desc.Options().(*descriptorpb.MessageOptions) - if !ok { - gen.Error(errors.New("get message options failed")) - } - worksheet, ok := proto.GetExtension(opts, tableaupb.E_Worksheet).(*tableaupb.WorksheetOptions) - if !ok { - gen.Error(errors.New("get worksheet extension failed")) - } - if worksheet != nil { + if options.IsWorksheet(message.Desc) { messagerName := string(message.Desc.Name()) messagers = append(messagers, messagerName) } diff --git a/test/csharp-tableau-loader/tests/HubFixture.cs b/test/csharp-tableau-loader/tests/HubFixture.cs index 575dcc9e..1b9ce03c 100644 --- a/test/csharp-tableau-loader/tests/HubFixture.cs +++ b/test/csharp-tableau-loader/tests/HubFixture.cs @@ -89,11 +89,10 @@ public HubFixture() } } - var options = new Tableau.HubOptions - { - Filter = name => name != "TaskConf", - }; - Hub = new Tableau.Hub(options); + // Full load (no filter) so the shared fixture is equivalent to the + // Go prepareHub / C++ HubFixture / TS prepareHub. The HubOptions.Filter + // feature is covered separately by LoadTests.Hub_Filter_LoadsOnlyMatchingMessagers. + Hub = new Tableau.Hub(); var loadOptions = new Tableau.Load.Options { diff --git a/test/csharp-tableau-loader/tests/LoadTests.cs b/test/csharp-tableau-loader/tests/LoadTests.cs index af6083b8..4e386159 100644 --- a/test/csharp-tableau-loader/tests/LoadTests.cs +++ b/test/csharp-tableau-loader/tests/LoadTests.cs @@ -32,11 +32,23 @@ public void Load_AllMessagers_Succeeds() } [Fact] - public void TaskConf_FilteredOut_IsNull() + public void Hub_Filter_LoadsOnlyMatchingMessagers() { - // HubFixture filters out TaskConf via HubOptions.Filter. - var taskConf = _hub.Get(); - Assert.Null(taskConf); + // A hub built with HubOptions.Filter loads only the matching + // messagers. Built as an isolated hub (not the shared fixture) so the + // shared fixture stays a full load, matching Go/C++/TS. Mirrors TS's + // "Hub filter" case in load.test.ts. + var options = new Tableau.HubOptions + { + Filter = name => name == "ItemConf", + }; + var filtered = new Tableau.Hub(options); + var loadOptions = new Tableau.Load.Options { IgnoreUnknownFields = true }; + bool ok = filtered.Load(TestPaths.ConfDir, Tableau.Format.JSON, loadOptions); + Assert.True(ok, $"filtered hub.Load failed: {Tableau.Util.GetErrMsg()}"); + + Assert.NotNull(filtered.GetItemConf()); + Assert.Null(filtered.GetActivityConf()); } // ---- CustomConf ---- From c58e5173800ed6a727410ff334b06606233eca19 Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 16 Jun 2026 17:28:30 +0800 Subject: [PATCH 2/4] ci(cpp): improve vcpkg caching for toolchain updates --- .github/workflows/testing-cpp.yml | 48 +++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/.github/workflows/testing-cpp.yml b/.github/workflows/testing-cpp.yml index 14078140..46fbd0ad 100644 --- a/.github/workflows/testing-cpp.yml +++ b/.github/workflows/testing-cpp.yml @@ -118,22 +118,54 @@ jobs: } EOF + - name: Compute vcpkg cache tag + # Capture the hosted-runner image release into a step output so the + # cache key below can rotate with the toolchain. We read the runner's + # `ImageVersion` process env var via bash (it isn't reliably exposed + # through the `env` expression context) and emit it as an output, which + # IS a supported context and always resolves. bash is available on all + # hosted runners, including Windows. + id: vcpkg_tag + shell: bash + run: echo "image=${ImageVersion:-noimg}" >> "$GITHUB_OUTPUT" + - name: Cache vcpkg_installed id: cache-vcpkg-installed uses: actions/cache@v4 with: path: ${{ env.VCPKG_INSTALLED_DIR }} - key: vcpkg-installed-${{ runner.os }}-${{ matrix.triplet }}-${{ matrix.config.vcpkg-commit }}-${{ hashFiles('test/cpp-tableau-loader/vcpkg.json') }} + # The image release is part of the key so the cache rotates with the + # toolchain. vcpkg keys its build artifacts by an ABI hash that + # includes the compiler/SDK version; windows-latest bumps MSVC often, + # invalidating that ABI. With a static key, a stale-ABI hit would + # restore an unusable tree and never re-save it (no save on + # primary-key hit), making vcpkg rebuild protobuf from source on + # EVERY Windows run. Linux's gcc is stable so it rarely rotates. + key: vcpkg-installed-${{ runner.os }}-${{ steps.vcpkg_tag.outputs.image }}-${{ matrix.triplet }}-${{ matrix.config.vcpkg-commit }}-${{ hashFiles('test/cpp-tableau-loader/vcpkg.json') }} + + - name: Export GitHub Actions cache env (for vcpkg x-gha) + # vcpkg's `x-gha` binary-cache provider reads these two vars to reach + # the Actions cache service; they aren't in step env by default. Only + # the modern row uses x-gha (see VCPKG_BINARY_SOURCES below). + if: matrix.config.label == 'modern' + uses: actions/github-script@v7 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Setup vcpkg & install protobuf - # Disable lukka/run-vcpkg's auto-injected `x-gha` binary cache: - # the `x-gha` provider only exists in vcpkg ≳ 2023, while our - # legacy matrix row pins a 2022-era vcpkg snapshot that errors - # out with "unknown binary provider type". `actions/cache` above - # already caches vcpkg_installed/, so we lose nothing here. - # `lukka/run-vcpkg` honours a pre-set VCPKG_BINARY_SOURCES. + # legacy-v3 pins a 2022-era vcpkg snapshot whose binary-cache code + # predates the `x-gha` provider and errors with "unknown binary + # provider type", so it keeps binary caching cleared and relies solely + # on the actions/cache of vcpkg_installed/ above. modern additionally + # enables `x-gha`, which keys cached package binaries by ABI hash and + # therefore self-heals when the runner bumps its toolchain (notably + # MSVC on windows-latest) — the actions/cache tree can't capture the + # compiler ABI in its key, x-gha can. `lukka/run-vcpkg` honours a + # pre-set VCPKG_BINARY_SOURCES. env: - VCPKG_BINARY_SOURCES: clear + VCPKG_BINARY_SOURCES: ${{ matrix.config.label == 'legacy-v3' && 'clear' || 'clear;x-gha,readwrite' }} uses: lukka/run-vcpkg@v11 with: vcpkgGitCommitId: ${{ matrix.config.vcpkg-commit }} From b6587cb0a378d6c1cfbb3b8b3ab81473b571b06e Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Tue, 16 Jun 2026 21:03:46 +0800 Subject: [PATCH 3/4] ci: remove comments from testing-cpp workflow --- .github/workflows/testing-cpp.yml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/.github/workflows/testing-cpp.yml b/.github/workflows/testing-cpp.yml index 46fbd0ad..5a049297 100644 --- a/.github/workflows/testing-cpp.yml +++ b/.github/workflows/testing-cpp.yml @@ -119,12 +119,6 @@ jobs: EOF - name: Compute vcpkg cache tag - # Capture the hosted-runner image release into a step output so the - # cache key below can rotate with the toolchain. We read the runner's - # `ImageVersion` process env var via bash (it isn't reliably exposed - # through the `env` expression context) and emit it as an output, which - # IS a supported context and always resolves. bash is available on all - # hosted runners, including Windows. id: vcpkg_tag shell: bash run: echo "image=${ImageVersion:-noimg}" >> "$GITHUB_OUTPUT" @@ -134,19 +128,9 @@ jobs: uses: actions/cache@v4 with: path: ${{ env.VCPKG_INSTALLED_DIR }} - # The image release is part of the key so the cache rotates with the - # toolchain. vcpkg keys its build artifacts by an ABI hash that - # includes the compiler/SDK version; windows-latest bumps MSVC often, - # invalidating that ABI. With a static key, a stale-ABI hit would - # restore an unusable tree and never re-save it (no save on - # primary-key hit), making vcpkg rebuild protobuf from source on - # EVERY Windows run. Linux's gcc is stable so it rarely rotates. key: vcpkg-installed-${{ runner.os }}-${{ steps.vcpkg_tag.outputs.image }}-${{ matrix.triplet }}-${{ matrix.config.vcpkg-commit }}-${{ hashFiles('test/cpp-tableau-loader/vcpkg.json') }} - name: Export GitHub Actions cache env (for vcpkg x-gha) - # vcpkg's `x-gha` binary-cache provider reads these two vars to reach - # the Actions cache service; they aren't in step env by default. Only - # the modern row uses x-gha (see VCPKG_BINARY_SOURCES below). if: matrix.config.label == 'modern' uses: actions/github-script@v7 with: @@ -155,15 +139,6 @@ jobs: core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); - name: Setup vcpkg & install protobuf - # legacy-v3 pins a 2022-era vcpkg snapshot whose binary-cache code - # predates the `x-gha` provider and errors with "unknown binary - # provider type", so it keeps binary caching cleared and relies solely - # on the actions/cache of vcpkg_installed/ above. modern additionally - # enables `x-gha`, which keys cached package binaries by ABI hash and - # therefore self-heals when the runner bumps its toolchain (notably - # MSVC on windows-latest) — the actions/cache tree can't capture the - # compiler ABI in its key, x-gha can. `lukka/run-vcpkg` honours a - # pre-set VCPKG_BINARY_SOURCES. env: VCPKG_BINARY_SOURCES: ${{ matrix.config.label == 'legacy-v3' && 'clear' || 'clear;x-gha,readwrite' }} uses: lukka/run-vcpkg@v11 From c23545a7178eee460055e59aa95516730ce8320d Mon Sep 17 00:00:00 2001 From: Kybxd <627940450@qq.com> Date: Wed, 17 Jun 2026 17:41:38 +0800 Subject: [PATCH 4/4] refactor(genhelper): unify MapKeySlice into a single generic type across loaders Define a single generic genhelper.MapKeySlice[F ParamFormatter] carrying all shared methods (AddMapKey/GenGetParams/GenGetArguments/GenCustom/ GenOtherArguments) as members. Each loader only provides a zero-size ParamFormatter and aliases the slice, removing duplicated per-language definitions of MapKeySlice and its methods across the cpp/csharp/go loaders. --- .../helper/helper.go | 28 ++++------- .../helper/helper.go | 31 +++--------- .../helper/helper.go | 22 +++------ internal/genhelper/mapkey.go | 49 ++++++++++++++++--- 4 files changed, 66 insertions(+), 64 deletions(-) diff --git a/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go b/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go index 9e515ebd..de18e11a 100644 --- a/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-cpp-tableau-loader/helper/helper.go @@ -162,28 +162,18 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect // MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. type MapKey = genhelper.MapKey -type MapKeySlice []MapKey +// cppParamFormatter formats a key as a C++ parameter declaration, using a const +// reference for std::string ("const std::string& name", "int32_t id"). +type cppParamFormatter struct{} -// AddMapKey appends a new map key, deduplicating Name and FieldName. -// See genhelper.AddMapKey for the full rationale. -func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - return genhelper.AddMapKey(s, newKey) +func (cppParamFormatter) FormatParam(key MapKey) string { + return ToConstRefType(key.Type) + " " + key.Name } -// GenGetParams generates function parameters, which are the names listed in the function's definition. -func (s MapKeySlice) GenGetParams() string { - return genhelper.GenCustom(s, func(key MapKey) string { return ToConstRefType(key.Type) + " " + key.Name }, ", ") -} - -// GenGetArguments generates function arguments, which are the real values passed to the function. -func (s MapKeySlice) GenGetArguments() string { - return genhelper.GenGetArguments(s) -} - -// GenOtherArguments generates function arguments for other value of std::tie. -func (s MapKeySlice) GenOtherArguments(other string) string { - return genhelper.GenCustom(s, func(key MapKey) string { return other + "." + key.Name }, ", ") -} +// MapKeySlice is the shared cross-language key slice (see genhelper.MapKeySlice) +// specialized with C++ parameter formatting. All slice methods (AddMapKey / +// GenGetParams / GenGetArguments / GenOtherArguments / ...) come from genhelper. +type MapKeySlice = genhelper.MapKeySlice[cppParamFormatter] func Indent(depth int) string { return strings.Repeat(" ", depth) diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go index b983fef0..96e8bd0b 100644 --- a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go @@ -305,32 +305,15 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect // MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. type MapKey = genhelper.MapKey -// MapKeySlice is an ordered collection of MapKey entries, providing methods -// to build function parameters, arguments, and custom formatted strings -// for code generation. -type MapKeySlice []MapKey +// csharpParamFormatter formats a key as a C# parameter declaration ("int id"). +type csharpParamFormatter struct{} -// AddMapKey appends a new map key, deduplicating Name and FieldName. -// See genhelper.AddMapKey for the full rationale. -func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - return genhelper.AddMapKey(s, newKey) -} - -// GenGetParams generates function parameters, which are the names listed in the function's definition. -func (s MapKeySlice) GenGetParams() string { - return s.GenCustom(func(key MapKey) string { return key.Type + " " + key.Name }, ", ") -} +func (csharpParamFormatter) FormatParam(key MapKey) string { return key.Type + " " + key.Name } -// GenGetArguments generates function arguments, which are the real values passed to the function. -func (s MapKeySlice) GenGetArguments() string { - return genhelper.GenGetArguments(s) -} - -// GenCustom generates a string by applying fn to each MapKey and joining -// the results with the given separator. Returns an empty string for empty slices. -func (s MapKeySlice) GenCustom(fn func(MapKey) string, sep string) string { - return genhelper.GenCustom(s, fn, sep) -} +// MapKeySlice is the shared cross-language key slice (see genhelper.MapKeySlice) +// specialized with C# parameter formatting. All slice methods (AddMapKey / +// GenGetParams / GenGetArguments / GenCustom / ...) come from genhelper. +type MapKeySlice = genhelper.MapKeySlice[csharpParamFormatter] // Indent returns a string of 4*depth spaces, used for indenting generated // C# code blocks at the specified nesting depth. diff --git a/cmd/protoc-gen-go-tableau-loader/helper/helper.go b/cmd/protoc-gen-go-tableau-loader/helper/helper.go index af588e17..762a0614 100644 --- a/cmd/protoc-gen-go-tableau-loader/helper/helper.go +++ b/cmd/protoc-gen-go-tableau-loader/helper/helper.go @@ -277,20 +277,12 @@ func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect // MapKey aliases the cross-language shared key descriptor; see genhelper.MapKey. type MapKey = genhelper.MapKey -type MapKeySlice []MapKey +// goParamFormatter formats a key as a Go parameter declaration ("id int32"). +type goParamFormatter struct{} -// AddMapKey appends a new map key, deduplicating Name and FieldName. -// See genhelper.AddMapKey for the full rationale. -func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { - return genhelper.AddMapKey(s, newKey) -} - -// GenGetParams generates function parameters, which are the names listed in the function's definition. -func (s MapKeySlice) GenGetParams() string { - return genhelper.GenCustom(s, func(key MapKey) string { return key.Name + " " + key.Type }, ", ") -} +func (goParamFormatter) FormatParam(key MapKey) string { return key.Name + " " + key.Type } -// GenGetArguments generates function arguments, which are the real values passed to the function. -func (s MapKeySlice) GenGetArguments() string { - return genhelper.GenGetArguments(s) -} +// MapKeySlice is the shared cross-language key slice (see genhelper.MapKeySlice) +// specialized with Go parameter formatting. All slice methods (AddMapKey / +// GenGetParams / GenGetArguments / ...) come from genhelper. +type MapKeySlice = genhelper.MapKeySlice[goParamFormatter] diff --git a/internal/genhelper/mapkey.go b/internal/genhelper/mapkey.go index 2b1e9bc8..a6863a3b 100644 --- a/internal/genhelper/mapkey.go +++ b/internal/genhelper/mapkey.go @@ -30,6 +30,31 @@ type MapKey struct { Fd protoreflect.FieldDescriptor } +// ParamFormatter renders a single MapKey as a function-parameter declaration in +// a target language. It is the ONLY piece of MapKeySlice behaviour that differs +// per language, e.g.: +// +// Go "id int32" (Name + " " + Type) +// C# "int id" (Type + " " + Name) +// C++ "int32_t id" (ToConstRefType(Type) + " " + Name) +// TS "id: number" (Name + ": " + Type) +// +// Each loader provides a zero-size implementation and wires it into MapKeySlice +// via the type parameter below, so every loader shares the exact same slice +// type and methods while only "overriding" parameter formatting. +type ParamFormatter interface { + FormatParam(MapKey) string +} + +// MapKeySlice is the single, cross-language ordered collection of MapKey shared +// by all protoc-gen-*-tableau-loader plugins. The type parameter F injects the +// language-specific parameter formatting used by GenGetParams; every other +// method (AddMapKey / GenGetArguments / GenCustom / GenOtherArguments) is fully +// shared. Loaders alias a concrete instantiation, e.g.: +// +// type MapKeySlice = genhelper.MapKeySlice[goParamFormatter] +type MapKeySlice[F ParamFormatter] []MapKey + // AddMapKey appends newKey to s, automatically deduplicating both Name (used as // function parameter names) and FieldName (used as struct field names in // LevelIndex key structs). @@ -52,7 +77,7 @@ type MapKey struct { // names, causing a compile error. With dedup, the conflicting name gets a // numeric suffix (the 1-based position of the new key in the slice), producing // valid code, e.g. "FruitType", "Id", "Id3". -func AddMapKey(s []MapKey, newKey MapKey) []MapKey { +func (s MapKeySlice[F]) AddMapKey(newKey MapKey) MapKeySlice[F] { if newKey.Name == "" { newKey.Name = fmt.Sprintf("key%d", len(s)+1) } @@ -78,18 +103,30 @@ func AddMapKey(s []MapKey, newKey MapKey) []MapKey { return append(s, newKey) } -// GenGetArguments generates function arguments, which are the real values -// passed to the function (i.e. the key Names joined by ", "). -func GenGetArguments(s []MapKey) string { - return GenCustom(s, func(key MapKey) string { return key.Name }, ", ") +// GenGetParams generates the function parameter list (declarations), formatted +// for the language carried by F (e.g. "id int32, name string"). +func (s MapKeySlice[F]) GenGetParams() string { + var f F + return s.GenCustom(f.FormatParam, ", ") +} + +// GenGetArguments generates the call argument list (the key Names joined by ", "). +func (s MapKeySlice[F]) GenGetArguments() string { + return s.GenCustom(func(key MapKey) string { return key.Name }, ", ") } // GenCustom builds a string by applying fn to each MapKey and joining the // results with sep. Returns an empty string for an empty slice. -func GenCustom(s []MapKey, fn func(MapKey) string, sep string) string { +func (s MapKeySlice[F]) GenCustom(fn func(MapKey) string, sep string) string { var params []string for _, key := range s { params = append(params, fn(key)) } return strings.Join(params, sep) } + +// GenOtherArguments generates arguments that access each key by Name on another +// object (e.g. "other.id, other.name"), used by C++ std::tie / hash combine. +func (s MapKeySlice[F]) GenOtherArguments(other string) string { + return s.GenCustom(func(key MapKey) string { return other + "." + key.Name }, ", ") +}