diff --git a/.github/workflows/release-csharp.yml b/.github/workflows/release-csharp.yml new file mode 100644 index 00000000..aa718903 --- /dev/null +++ b/.github/workflows/release-csharp.yml @@ -0,0 +1,62 @@ +name: Release C# + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + release: + name: Release cmd/protoc-gen-csharp-tableau-loader + runs-on: ubuntu-latest + if: startsWith(github.event.release.tag_name, 'cmd/protoc-gen-csharp-tableau-loader/') + strategy: + matrix: + goos: [linux, darwin, windows] + goarch: [amd64, arm64] + exclude: + - goos: linux + goarch: arm64 + - goos: windows + goarch: arm64 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version: "1.21.x" + + - name: Download dependencies + run: | + cd cmd/protoc-gen-csharp-tableau-loader + go mod download + - name: Prepare build directory + run: | + mkdir -p build/ + cp README.md build/ + cp LICENSE build/ + - name: Build + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + run: | + cd cmd/protoc-gen-csharp-tableau-loader + go build -trimpath -o $GITHUB_WORKSPACE/build + - name: Create package + id: package + run: | + PACKAGE_NAME=protoc-gen-csharp-tableau-loader.${GITHUB_REF#refs/tags/cmd/protoc-gen-csharp-tableau-loader/}.${{ matrix.goos }}.${{ matrix.goarch }}.tar.gz + tar -czvf $PACKAGE_NAME -C build . + echo ::set-output name=name::${PACKAGE_NAME} + - name: Upload asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ./${{ steps.package.outputs.name }} + asset_name: ${{ steps.package.outputs.name }} + asset_content_type: application/gzip diff --git a/.github/workflows/testing-csharp.yml b/.github/workflows/testing-csharp.yml new file mode 100644 index 00000000..b2c5fd5c --- /dev/null +++ b/.github/workflows/testing-csharp.yml @@ -0,0 +1,69 @@ +name: Testing C# + +# Trigger on pushes, PRs (excluding documentation changes), and nightly. +on: + push: + branches: [master, main] + pull_request: + schedule: + - cron: 0 0 * * * # daily at 00:00 + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + strategy: + matrix: + include: + - os: ubuntu-latest + gen_script: bash gen.sh + - os: windows-latest + gen_script: cmd /c gen.bat + + runs-on: ${{ matrix.os }} + timeout-minutes: 10 + + steps: + - name: Checkout Code + uses: actions/checkout@v6 + + - name: Install Go + uses: actions/setup-go@v6 + with: + go-version: 1.21.x + cache: true + + - name: Install .NET SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: choco install cmake ninja -y + + - name: Setup MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + + - name: Init submodules and build protobuf + shell: bash + run: bash init.sh + + - name: Generate protoconf + working-directory: test/csharp-tableau-loader + run: ${{ matrix.gen_script }} + + - name: Build + working-directory: test/csharp-tableau-loader + run: dotnet build + + - name: Run loader + working-directory: test/csharp-tableau-loader + run: dotnet run diff --git a/.gitignore b/.gitignore index 9c0a1db2..de4ff9c5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,7 +39,11 @@ node_modules cmd/protoc-gen-cpp-tableau-loader/protoc-gen-cpp-tableau-loader cmd/protoc-gen-go-tableau-loader/protoc-gen-go-tableau-loader +cmd/protoc-gen-csharp-tableau-loader/protoc-gen-csharp-tableau-loader test/go-tableau-loader/go-tableau-loader _lab/ts/src/protoconf coverage.txt +!test/testdata/bin/ +test/csharp-tableau-loader/protoconf + diff --git a/README.md b/README.md index 4ff8e00d..396a469c 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,20 @@ The official config loader for [Tableau](https://github.com/tableauio/tableau). - [Protocol Buffers Go Reference](https://protobuf.dev/reference/go/) +## C# + +### Requirements + +- Unity 2022.3 LTS (C# 9) +- dotnet-sdk-8.0 + +### Test + +- Install: **dotnet-sdk-8.0** +- Change dir: `cd test/csharp-tableau-loader` +- Generate protoconf: `sh gen.sh` +- Test: `dotnet run` + ## TypeScript ### Requirements diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed.go b/cmd/protoc-gen-csharp-tableau-loader/embed.go new file mode 100644 index 00000000..9961feda --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/embed.go @@ -0,0 +1,34 @@ +package main + +import ( + "embed" + "path" + + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "google.golang.org/protobuf/compiler/protogen" +) + +//go:embed embed/* +var efs embed.FS + +// generateEmbed generates related registry files. +func generateEmbed(gen *protogen.Plugin) { + entries, err := efs.ReadDir("embed") + if err != nil { + panic(err) + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + + g := gen.NewGeneratedFile(entry.Name(), "") + helper.GenerateFileHeader(gen, nil, g, version) + // refer: [embed: embed path on different OS cannot open file](https://github.com/golang/go/issues/45230) + content, err := efs.ReadFile(path.Join("embed", entry.Name())) + if err != nil { + panic(err) + } + g.P(string(content)) + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs new file mode 100644 index 00000000..8990b929 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/embed/Load.pc.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using System.IO; +using pb = global::Google.Protobuf; +using pbr = global::Google.Protobuf.Reflection; +namespace Tableau +{ + /// + /// Load provides functions for loading and parsing protobuf messages from files. + /// + public static class Load + { + /// + /// ReadFunc reads the config file and returns its content. + /// + public delegate byte[] ReadFunc(string path); + /// + /// LoadFunc defines a func which can load message's content based on the + /// given descriptor, path, format, and options. + /// + public delegate pb::IMessage? LoadFunc(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options); + + /// + /// BaseOptions is the common options for both global-level and messager-level options. + /// + public class BaseOptions + { + /// + /// Whether to ignore unknown JSON fields during parsing. + /// + public bool? IgnoreUnknownFields { get; set; } + /// + /// You can specify custom read function to read a config file's content. + /// Default is File.ReadAllBytes. + /// + public ReadFunc? ReadFunc { get; set; } + /// + /// You can specify custom load function to load a messager's content. + /// Default is LoadMessager. + /// + public LoadFunc? LoadFunc { get; set; } + } + + /// + /// Options is the global-level options, which contains both global-level and + /// messager-level options. + /// + public class Options : BaseOptions + { + /// + /// MessagerOptions maps each messager name to a MessagerOptions. + /// If specified, then the messager will be parsed with the given options directly. + /// + public Dictionary? MessagerOptions { get; set; } + /// + /// ParseMessagerOptionsByName parses messager options with both global-level and + /// messager-level options taken into consideration. + /// + public MessagerOptions ParseMessagerOptionsByName(string name) + { + var mopts = MessagerOptions?.TryGetValue(name, out var val) == true ? (MessagerOptions)val.Clone() : new MessagerOptions(); + mopts.IgnoreUnknownFields ??= IgnoreUnknownFields; + mopts.ReadFunc ??= ReadFunc; + mopts.LoadFunc ??= LoadFunc; + return mopts; + } + } + + /// + /// MessagerOptions defines the options for loading a messager. + /// + public class MessagerOptions : BaseOptions, ICloneable + { + /// + /// Path maps each messager name to a corresponding config file path. + /// If specified, then the main messager will be parsed from the file + /// directly, other than the specified load dir. + /// + public string? Path { get; set; } + + public object Clone() + { + return new MessagerOptions + { + IgnoreUnknownFields = IgnoreUnknownFields, + ReadFunc = ReadFunc, + LoadFunc = LoadFunc, + Path = Path + }; + } + } + + /// + /// LoadMessager loads a protobuf message from the specified file path and format. + /// + public static pb::IMessage? LoadMessager(pbr::MessageDescriptor desc, string path, Format fmt, in MessagerOptions? options = null) + { + var readFunc = options?.ReadFunc ?? File.ReadAllBytes; + byte[] content; + try + { + content = readFunc(path); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to read {path}: {ex.Message}"); + throw; + } + return Unmarshal(content, desc, fmt, options); + } + + /// + /// LoadMessagerInDir loads a protobuf message from the specified directory and format. + /// It resolves the file path based on the message descriptor name. + /// + public static pb::IMessage? LoadMessagerInDir(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options = null) + { + string name = desc.Name; + string path = ""; + if (options?.Path != null) + { + path = options.Path; + fmt = Util.GetFormat(path); + } + if (path == "") + { + string filename = name + Util.Format2Ext(fmt); + path = Path.Combine(dir, filename); + } + var loadFunc = options?.LoadFunc ?? LoadMessager; + return loadFunc(desc, path, fmt, options); + } + + /// + /// Unmarshal parses the given byte content into a protobuf message based on the specified format. + /// + public static pb::IMessage? Unmarshal(byte[] content, pbr::MessageDescriptor desc, Format fmt, in MessagerOptions? options = null) + { + switch (fmt) + { + case Format.JSON: + try + { + var parser = new pb::JsonParser( + pb::JsonParser.Settings.Default.WithIgnoreUnknownFields(options?.IgnoreUnknownFields ?? false) + ); + return parser.Parse(new StreamReader(new MemoryStream(content)), desc); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to parse {desc.Name}.json: {ex.Message}"); + throw; + } + case Format.Bin: + try + { + return desc.Parser.ParseFrom(content); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to parse {desc.Name}.binpb: {ex.Message}"); + throw; + } + default: + Util.SetErrMsg($"unknown format: {fmt}"); + return null; + } + } + } + + /// + /// IMessagerName is an interface that provides the Name() method for messagers. + /// + public interface IMessagerName + { + string Name(); + } + + /// + /// Messager is the base class for all generated configuration messagers. + /// It is designed for three goals: + /// 1. Easy use: simple yet powerful accessors. + /// 2. Elegant API: concise and clean functions. + /// 3. Extensibility: Map, OrderedMap, Index, OrderedIndex... + /// + public abstract class Messager + { + /// + /// Stats contains statistics info about loading. + /// + public class Stats + { + /// Total load time consuming. + public TimeSpan Duration; + } + + protected Stats LoadStats = new(); + + /// + /// GetStats returns the loading stats info. + /// + public ref readonly Stats GetStats() => ref LoadStats; + + /// + /// Load fills message from file in the specified directory and format. + /// + public abstract bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null); + + /// + /// Message returns the inner protobuf message data. + /// + public virtual pb::IMessage? Message() => null; + + /// + /// ProcessAfterLoad is invoked after this messager loaded. + /// + protected virtual bool ProcessAfterLoad() => true; + + /// + /// ProcessAfterLoadAll is invoked after all messagers loaded. + /// + public virtual bool ProcessAfterLoadAll(in Hub hub) => true; + } +} \ No newline at end of file diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs b/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs new file mode 100644 index 00000000..50644046 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/embed/Util.pc.cs @@ -0,0 +1,63 @@ +using System; +using System.IO; +namespace Tableau +{ + /// + /// Format specifies the format of the configuration file. + /// + public enum Format + { + Unknown, + JSON, + Bin + } + + /// + /// Util provides common utility functions. + /// + public static class Util + { + [ThreadStatic] private static string? _errMsg; + + /// + /// Get the last error message. + /// + public static string GetErrMsg() => _errMsg ?? ""; + + /// + /// Set the error message. + /// + public static void SetErrMsg(string msg) => _errMsg = msg; + + private const string _unknownExt = ".unknown"; + private const string _jsonExt = ".json"; + private const string _binExt = ".binpb"; + + /// + /// GetFormat returns the Format type determined by the file extension of the given path. + /// + public static Format GetFormat(string path) + { + string ext = Path.GetExtension(path); + return ext switch + { + _jsonExt => Format.JSON, + _binExt => Format.Bin, + _ => Format.Unknown, + }; + } + + /// + /// Format2Ext returns the file extension corresponding to the given format. + /// + public static string Format2Ext(Format fmt) + { + return fmt switch + { + Format.JSON => _jsonExt, + Format.Bin => _binExt, + _ => _unknownExt, + }; + } + } +} \ No newline at end of file diff --git a/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl new file mode 100644 index 00000000..92ddbdfb --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/embed/templates/Hub.pc.cs.tpl @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// MessagerContainer holds all messager instances and provides fast access. + /// + internal class MessagerContainer + { + public Dictionary MessagerMap; + public DateTime LastLoadedTime; +{{ range . }} public {{ . }}? {{ . }}; +{{ end }} + public MessagerContainer(Dictionary? messagerMap = null) + { + MessagerMap = messagerMap ?? new Dictionary(); + LastLoadedTime = DateTime.Now; +{{ range . }} {{ . }} = InternalGet<{{ . }}>(messagerMap); +{{ end }} } + + /// + /// Get returns the messager of type T from the container. + /// + public T? Get() where T : Messager, IMessagerName, new() => InternalGet(MessagerMap); + + private static T? InternalGet(Dictionary? messagerMap) where T : Messager, IMessagerName, new() + { + if (messagerMap == null) return null; + string name = new T().Name(); + return messagerMap.TryGetValue(name, out var messager) ? (T)messager : null; + } + } + + /// + /// Atomic provides a thread-safe wrapper for reference types. + /// + internal class Atomic where T : class + { + private T? _value; + + public T? Value + { + get => Interlocked.CompareExchange(ref _value, null, null); + set => Interlocked.Exchange(ref _value, value); + } + } + + /// + /// HubOptions is the options for Hub. + /// + public class HubOptions + { + /// + /// Filter can only filter in certain specific messagers based on the + /// condition that you provide. + /// + public Func? Filter { get; set; } + } + + /// + /// Hub is the messager manager. It manages loading, accessing, and storing + /// all configuration messagers. + /// + public class Hub + { + private readonly Atomic _messagerContainer = new Atomic(); + private readonly HubOptions? _options; + + public Hub(HubOptions? options = null) + { + _options = options; + } + + /// + /// Load fills messages from files in the specified directory and format. + /// + public bool Load(string dir, Format fmt, in Load.Options? options = null) + { + var messagerMap = NewMessagerMap(); + var opts = options ?? new Load.Options(); + foreach (var kvs in messagerMap) + { + string name = kvs.Key; + if (!kvs.Value.Load(dir, fmt, opts.ParseMessagerOptionsByName(name))) + { + Console.Error.WriteLine($"load {name} failed: {Util.GetErrMsg()}"); + return false; + } + } + var tmpHub = new Hub(); + tmpHub.SetMessagerMap(messagerMap); + foreach (var messager in messagerMap) + { + if (!messager.Value.ProcessAfterLoadAll(tmpHub)) + { + Console.Error.WriteLine($"hub call ProcessAfterLoadAll failed, messager: {messager.Key}"); + return false; + } + } + SetMessagerMap(messagerMap); + return true; + } + + /// + /// GetMessagerMap returns the current messager map. + /// + public IReadOnlyDictionary? GetMessagerMap() => _messagerContainer.Value?.MessagerMap; + + /// + /// SetMessagerMap sets the messager map with thread-safe guarantee. + /// + public void SetMessagerMap(in Dictionary map) => _messagerContainer.Value = new MessagerContainer(map); + + /// + /// Get returns the messager of type T from the hub. + /// + public T? Get() where T : Messager, IMessagerName, new() => _messagerContainer.Value?.Get(); +{{ range . }} + public {{ . }}? Get{{ . }}() => _messagerContainer.Value?.{{ . }}; +{{ end }} + /// + /// GetLastLoadedTime returns the time when hub's messager container was last set. + /// + public DateTime? GetLastLoadedTime() => _messagerContainer.Value?.LastLoadedTime; + + /// + /// NewMessagerMap creates a new MessagerMap based on the registered messagers. + /// + private Dictionary NewMessagerMap() + { + var messagerMap = new Dictionary(); + foreach (var kv in Registry.Registrar) + { + if (_options?.Filter?.Invoke(kv.Key) ?? true) + { + messagerMap[kv.Key] = kv.Value(); + } + } + return messagerMap; + } + } + + /// + /// Registry manages the registration of all messager generators. + /// + public class Registry + { + internal static readonly Dictionary> Registrar = new Dictionary>(); + + /// + /// Register registers a messager generator for type T. + /// + public static void Register() where T : Messager, IMessagerName, new() => Registrar[new T().Name()] = () => new T(); + + /// + /// Init registers all generated messagers. + /// + public static void Init() + { +{{ range . }} Register<{{ . }}>(); +{{ end }} } + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go new file mode 100644 index 00000000..e3d2cede --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/helper/helper.go @@ -0,0 +1,278 @@ +package helper + +import ( + "fmt" + "strings" + + "github.com/iancoleman/strcase" + "github.com/tableauio/tableau/proto/tableaupb" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +func GenerateFileHeader(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile, version string) { + g.P("// ") + 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("// ") + g.P("#nullable enable") +} + +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 ParseIndexFieldName(fd protoreflect.FieldDescriptor) string { + return ParseCsharpPropertyName(fd) +} + +// ParseCsharpPropertyName converts a protobuf field descriptor to the corresponding +// C# property name, matching the behavior of protoc's C# code generator. +// When the PascalCase field name collides with its containing type name, +// "Types", or "Descriptor", a trailing underscore is appended to avoid conflicts. +// See: protobuf/src/google/protobuf/compiler/csharp/csharp_helpers.cc GetPropertyName() +func ParseCsharpPropertyName(fd protoreflect.FieldDescriptor) string { + propertyName := strcase.ToCamel(string(fd.Name())) + containingName := string(fd.Parent().(protoreflect.MessageDescriptor).Name()) + if propertyName == containingName || propertyName == "Types" || propertyName == "Descriptor" { + propertyName += "_" + } + return propertyName +} + +func ParseIndexFieldNameAsKeyStructFieldName(fd protoreflect.FieldDescriptor) string { + if fd.IsList() { + opts := fd.Options().(*descriptorpb.FieldOptions) + fdOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions) + return strcase.ToCamel(fdOpts.GetName()) + } + // NOTE: Do not use ParseCsharpPropertyName here, because key struct field names + // are defined in our own code (not protobuf-generated), so they are not subject + // to protoc's property name conflict resolution (appending "_" suffix). + return strcase.ToCamel(string(fd.Name())) +} + +func ParseIndexFieldNameAsFuncParam(fd protoreflect.FieldDescriptor) string { + return escapeIdentifier(strcase.ToLowerCamel(ParseIndexFieldNameAsKeyStructFieldName(fd))) +} + +// ParseCsharpType converts a FieldDescriptor to C# type string. +func ParseCsharpType(fd protoreflect.FieldDescriptor) string { + switch fd.Kind() { + case protoreflect.BoolKind: + return "bool" + case protoreflect.EnumKind: + fullname := string(fd.Enum().FullName()) + seps := strings.Split(fullname, ".") + seps[0] = strcase.ToCamel(seps[0]) + for i := 2; i < len(seps); i++ { + seps[i] = "Types." + seps[i] + } + return strings.Join(seps, ".") + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return "int" + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return "uint" + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return "long" + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return "ulong" + case protoreflect.FloatKind: + return "float" + case protoreflect.DoubleKind: + return "double" + case protoreflect.StringKind, protoreflect.BytesKind: + return "string" + case protoreflect.MessageKind: + return ParseCsharpClassType(fd.Message()) + // case protoreflect.GroupKind: + // return "group" + default: + return fmt.Sprintf("", fd.Kind()) + } +} + +func ParseCsharpClassType(md protoreflect.MessageDescriptor) string { + ns := ParseCsharpNamespace(md.ParentFile()) + name := getNestedName(md) + if ns != "" { + return ns + "." + name + } + return name +} + +func getNestedName(md protoreflect.MessageDescriptor) string { + parent, ok := md.Parent().(protoreflect.MessageDescriptor) + if ok { + return getNestedName(parent) + ".Types." + string(md.Name()) + } + return string(md.Name()) +} + +func ParseCsharpNamespace(fd protoreflect.FileDescriptor) string { + if opts, ok := fd.Options().(*descriptorpb.FileOptions); ok && opts != nil { + if ns := opts.GetCsharpNamespace(); ns != "" { + return ns + } + } + return strcase.ToCamel(string(fd.Package())) +} + +// ParseOrderedIndexKeyType converts a FieldDescriptor to its treemap key type. +// fd must be an ordered type, or a message which can be converted to an ordered type. +func ParseOrderedIndexKeyType(fd protoreflect.FieldDescriptor) string { + switch fd.Kind() { + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + return "int" + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + return "uint" + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + return "long" + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + return "ulong" + case protoreflect.FloatKind: + return "float" + case protoreflect.DoubleKind: + return "double" + case protoreflect.StringKind: + return "string" + case protoreflect.EnumKind: + fullname := string(fd.Enum().FullName()) + seps := strings.Split(fullname, ".") + seps[0] = strcase.ToCamel(seps[0]) + for i := 2; i < len(seps); i++ { + seps[i] = "Types." + seps[i] + } + return strings.Join(seps, ".") + case protoreflect.MessageKind: + switch fd.Message().FullName() { + case "google.protobuf.Timestamp", "google.protobuf.Duration": + return "long" + default: + } + fallthrough + default: + panic(fmt.Sprintf("unsupported kind: %d", fd.Kind())) + } +} + +func ParseMapFieldNameAsKeyStructFieldName(fd protoreflect.FieldDescriptor) string { + opts := fd.Options().(*descriptorpb.FieldOptions) + fdOpts := proto.GetExtension(opts, tableaupb.E_Field).(*tableaupb.FieldOptions) + name := fdOpts.GetKey() + if fd.MapValue().Kind() == protoreflect.MessageKind { + valueFd := fd.MapValue().Message().Fields().Get(0) + name = string(valueFd.Name()) + } + return strcase.ToCamel(name) +} + +func ParseMapFieldNameAsFuncParam(fd protoreflect.FieldDescriptor) string { + fieldName := ParseMapFieldNameAsKeyStructFieldName(fd) + if fieldName == "" { + return fieldName + } + return escapeIdentifier(fieldName) +} + +func ParseMapKeyType(fd protoreflect.FieldDescriptor) string { + return ParseCsharpType(fd) +} + +func ParseMapValueType(fd protoreflect.FieldDescriptor) string { + return ParseCsharpType(fd.MapValue()) +} + +func GetTypeEmptyValue(fd protoreflect.FieldDescriptor) string { + switch fd.Kind() { + case protoreflect.BoolKind: + return "false" + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind, + protoreflect.Uint32Kind, protoreflect.Fixed32Kind, + protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind, + protoreflect.Uint64Kind, protoreflect.Fixed64Kind, protoreflect.EnumKind: + return "0" + case protoreflect.FloatKind, protoreflect.DoubleKind: + return "0.0" + case protoreflect.StringKind: + return `""` + case protoreflect.BytesKind, protoreflect.MessageKind: + return "null" + // case protoreflect.GroupKind: + // return "group" + default: + return fmt.Sprintf("", fd.Kind()) + } +} + +func ParseLeveledMapPrefix(md protoreflect.MessageDescriptor, mapFd protoreflect.FieldDescriptor) string { + if mapFd.MapValue().Kind() == protoreflect.MessageKind { + localMsgProtoName := strings.TrimPrefix(string(mapFd.MapValue().Message().FullName()), string(md.FullName())+".") + return strings.ReplaceAll(localMsgProtoName, ".", "_") + } + return mapFd.MapValue().Kind().String() +} + +type MapKey struct { + Type string + Name string + FieldName string // multi-colunm index only + Fd protoreflect.FieldDescriptor // the map field descriptor this key belongs to +} + +type MapKeySlice []MapKey + +func (s MapKeySlice) AddMapKey(newKey MapKey) MapKeySlice { + if newKey.Name == "" { + newKey.Name = fmt.Sprintf("key%d", len(s)+1) + } + for _, key := range s { + if key.Name == newKey.Name { + // rewrite to avoid name confict + newKey.Name = fmt.Sprintf("%s%d", newKey.Name, len(s)+1) + break + } + } + return append(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 }, ", ") +} + +// 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 }, ", ") +} + +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) +} + +func Indent(depth int) string { + return strings.Repeat(" ", depth) +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go b/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go new file mode 100644 index 00000000..7599c731 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/helper/keyword.go @@ -0,0 +1,117 @@ +package helper + +import ( + "strings" + "unicode" + + "github.com/iancoleman/strcase" +) + +var csharpKeywords map[string]bool + +func escapeIdentifier(str string) string { + // Filter invalid runes + var result strings.Builder + for _, r := range str { + if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' { + result.WriteRune(r) + } + } + str = result.String() + // To camel case + str = strcase.ToLowerCamel(str) + // Csharp variables must not start with digits + if len(str) != 0 && unicode.IsDigit(rune(str[0])) { + str = "_" + str + } + // Avoid csharp keywords + if _, ok := csharpKeywords[str]; ok { + return str + "_" + } + return str +} + +// Ref: +// +// https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords +func init() { + csharpKeywords = map[string]bool{ + "abstract": true, + "as": true, + "base": true, + "bool": true, + "break": true, + "byte": true, + "case": true, + "catch": true, + "char": true, + "checked": true, + "class": true, + "const": true, + "continue": true, + "decimal": true, + "default": true, + "delegate": true, + "do": true, + "double": true, + "else": true, + "enum": true, + "event": true, + "explicit": true, + "extern": true, + "false": true, + "finally": true, + "fixed": true, + "float": true, + "for": true, + "foreach": true, + "goto": true, + "if": true, + "implicit": true, + "in": true, + "int": true, + "interface": true, + "internal": true, + "is": true, + "lock": true, + "long": true, + "namespace": true, + "new": true, + "null": true, + "object": true, + "operator": true, + "out": true, + "override": true, + "params": true, + "private": true, + "protected": true, + "public": true, + "readonly": true, + "ref": true, + "return": true, + "sbyte": true, + "sealed": true, + "short": true, + "sizeof": true, + "stackalloc": true, + "static": true, + "string": true, + "struct": true, + "switch": true, + "this": true, + "throw": true, + "true": true, + "try": true, + "typeof": true, + "uint": true, + "ulong": true, + "unchecked": true, + "unsafe": true, + "ushort": true, + "using": true, + "virtual": true, + "void": true, + "volatile": true, + "while": true, + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/hub.go b/cmd/protoc-gen-csharp-tableau-loader/hub.go new file mode 100644 index 00000000..da474435 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/hub.go @@ -0,0 +1,21 @@ +package main + +import ( + "text/template" + + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/internal/extensions" + "google.golang.org/protobuf/compiler/protogen" +) + +var tpl = template.Must(template.New("").ParseFS(efs, "embed/templates/*")) + +// generateHub generates related hub files. +func generateHub(gen *protogen.Plugin) { + filename := "Hub." + extensions.PC + ".cs" + g := gen.NewGeneratedFile(filename, "") + helper.GenerateFileHeader(gen, nil, g, version) + if err := tpl.Lookup(filename+".tpl").Execute(g, messagers); err != nil { + panic(err) + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go new file mode 100644 index 00000000..29f84787 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/generator.go @@ -0,0 +1,154 @@ +package indexes + +import ( + "fmt" + + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/internal/index" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type Generator struct { + g *protogen.GeneratedFile + descriptor *index.IndexDescriptor + message *protogen.Message + + // level message + keys helper.MapKeySlice +} + +func NewGenerator(g *protogen.GeneratedFile, descriptor *index.IndexDescriptor, message *protogen.Message) *Generator { + generator := &Generator{ + g: g, + descriptor: descriptor, + message: message, + } + generator.initLevelMessage() + return generator +} + +func (x *Generator) initLevelMessage() { + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + if fd := lm.FD; fd != nil && fd.IsMap() { + // Only collect map keys/fds when a deeper level has an index or ordered index, + // because these keys are used solely for building upper-level (leveled) containers. + if !lm.NextLevel.NeedGenAnyIndex() { + break + } + x.keys = x.keys.AddMapKey(helper.MapKey{ + Type: helper.ParseMapKeyType(fd.MapKey()), + Name: helper.ParseMapFieldNameAsFuncParam(fd), + FieldName: helper.ParseMapFieldNameAsKeyStructFieldName(fd), + Fd: fd, + }) + } + } +} + +func (x *Generator) NeedGenerate() bool { + return x.needGenerateIndex() || x.needGenerateOrderedIndex() +} + +func (x *Generator) levelKeyType(mapFd protoreflect.FieldDescriptor) string { + return fmt.Sprintf("LevelIndex_%sKey", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd)) +} + +func (x *Generator) mapValueType(index *index.LevelIndex) string { + return helper.ParseCsharpClassType(index.MD) +} + +func (x *Generator) fieldGetter(fd protoreflect.FieldDescriptor) string { + return fmt.Sprintf(".%s", helper.ParseIndexFieldName(fd)) +} + +func (x *Generator) parseKeyFieldNameAndSuffix(field *index.LevelField) (string, string) { + var fieldName, suffix string + needEmptyValue := len(field.LeveledFDList) > 1 + for i, leveledFd := range field.LeveledFDList { + if i != 0 { + fieldName += "?" + } + fieldName += x.fieldGetter(leveledFd) + if i == len(field.LeveledFDList)-1 && leveledFd.Message() != nil { + switch leveledFd.Message().FullName() { + case "google.protobuf.Timestamp", "google.protobuf.Duration": + suffix = "?.Seconds ?? 0" + needEmptyValue = false + default: + } + } + } + if field.FD.IsList() { + fieldName += " ?? Enumerable.Empty<" + helper.ParseCsharpType(field.FD) + ">()" + } else if needEmptyValue { + fieldName += " ?? " + helper.GetTypeEmptyValue(field.FD) + } + return fieldName, suffix +} + +func (x *Generator) GenIndexTypeDef() { + if !x.NeedGenerate() { + return + } + // Generate LevelIndex key structs for intermediate map levels. + // + // x.keys holds one entry per map level whose next level still needs an + // index (populated by initLevelMessage). For a 3-level map keyed by + // (k1, k2, k3) with an index at the deepest level, x.keys = [k1, k2, k3]. + // + // Level containers at depth 1 are keyed by a single scalar (k1), so no + // composite key struct is needed. The deepest level (depth = len(keys)) + // also does not need one, because its full key combination (all keys) + // is already represented by the index's own key struct generated + // separately. Only intermediate depths (2 ≤ depth < len(keys)) require + // a LevelIndex struct that bundles all ancestor keys up to that depth: + // + // keys = [k1, k2, k3] → struct for depth 2: {k1, k2} + // keys = [k1, k2, k3, k4] → struct for depth 2: {k1, k2} + // struct for depth 3: {k1, k2, k3} + // + // The loop starts at i=2 (depth 2) and creates a struct from keys[:i]. + // It runs len(x.keys)-2 times (0 times when len ≤ 2). + for i := 2; i < len(x.keys); i++ { + if i == 2 { + x.g.P() + x.g.P(helper.Indent(2), "// LevelIndex keys.") + } + fd := x.keys[i-1].Fd + keyType := x.levelKeyType(fd) + x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IEquatable<", keyType, ">") + x.g.P(helper.Indent(2), "{") + keys := x.keys[:i] + for _, key := range keys { + x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }") + } + x.g.P() + x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")") + x.g.P(helper.Indent(3), "{") + for _, key := range keys { + x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";") + } + x.g.P(helper.Indent(3), "}") + x.g.P() + x.g.P(helper.Indent(3), "public bool Equals(", keyType, " other) =>") + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").Equals((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));") + x.g.P() + x.g.P(helper.Indent(3), "public override int GetHashCode() =>") + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").GetHashCode();") + x.g.P(helper.Indent(2), "}") + x.g.P() + } + x.genIndexTypeDef() + x.genOrderedIndexTypeDef() +} + +func (x *Generator) GenIndexLoader() { + x.genIndexLoader() + x.genOrderedIndexLoader() +} + +func (x *Generator) GenIndexFinders() { + x.genIndexFinders() + x.genOrderedIndexFinders() +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go new file mode 100644 index 00000000..d5fb3a7f --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/index.go @@ -0,0 +1,344 @@ +package indexes + +import ( + "fmt" + "strings" + "sync" + + "github.com/iancoleman/strcase" + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/internal/index" + "github.com/tableauio/loader/internal/loadutil" + "github.com/tableauio/loader/internal/options" +) + +func (x *Generator) needGenerateIndex() bool { + return options.NeedGenIndex(x.message.Desc, options.LangCS) +} + +func (x *Generator) indexMapType(index *index.LevelIndex) string { + return fmt.Sprintf("Index_%sMap", index.Name()) +} + +func (x *Generator) indexMapKeyType(index *index.LevelIndex) string { + if len(index.ColFields) == 1 { + // single-column index + field := index.ColFields[0] // just take first field + return helper.ParseCsharpType(field.FD) + } else { + // multi-column index + return fmt.Sprintf("Index_%sKey", index.Name()) + } +} + +func (x *Generator) indexContainerName(index *index.LevelIndex, i int) string { + if i == 0 { + return fmt.Sprintf("_index%sMap", strcase.ToCamel(index.Name())) + } + return fmt.Sprintf("_index%sMap%d", strcase.ToCamel(index.Name()), i) +} + +func (x *Generator) indexKeys(index *index.LevelIndex) helper.MapKeySlice { + var keys helper.MapKeySlice + for _, field := range index.ColFields { + keys = keys.AddMapKey(helper.MapKey{ + Type: helper.ParseCsharpType(field.FD), + Name: helper.ParseIndexFieldNameAsFuncParam(field.FD), + FieldName: helper.ParseIndexFieldNameAsKeyStructFieldName(field.FD), + }) + } + return keys +} + +func (x *Generator) genIndexTypeDef() { + if !x.needGenerateIndex() { + return + } + var once sync.Once + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.Indexes { + once.Do(func() { x.g.P(helper.Indent(2), "// Index types.") }) + x.g.P(helper.Indent(2), "// Index: ", index.Index) + mapType := x.indexMapType(index) + keyType := x.indexMapKeyType(index) + valueType := x.mapValueType(index) + keys := x.indexKeys(index) + if len(index.ColFields) != 1 { + // Generate key struct + x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IEquatable<", keyType, ">") + x.g.P(helper.Indent(2), "{") + for _, key := range keys { + x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }") + } + x.g.P() + x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")") + x.g.P(helper.Indent(3), "{") + for _, key := range keys { + x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";") + } + x.g.P(helper.Indent(3), "}") + x.g.P() + x.g.P(helper.Indent(3), "public bool Equals(", keyType, " other) =>") + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").Equals((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));") + x.g.P() + x.g.P(helper.Indent(3), "public override int GetHashCode() =>") + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").GetHashCode();") + x.g.P(helper.Indent(2), "}") + x.g.P() + } + x.g.P(helper.Indent(2), "public class ", mapType, " : Dictionary<", keyType, ", List<", valueType, ">> { }") + x.g.P() + + x.g.P(helper.Indent(2), "private ", mapType, " ", x.indexContainerName(index, 0), " = new ", mapType, "();") + x.g.P() + for i := 1; i < lm.MapDepth; i++ { + if i == 1 { + x.g.P(helper.Indent(2), "private Dictionary<", x.keys[0].Type, ", ", mapType, "> ", x.indexContainerName(index, i), " = new Dictionary<", x.keys[0].Type, ", ", mapType, ">();") + } else { + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(2), "private Dictionary<", levelIndexKeyType, ", ", mapType, "> ", x.indexContainerName(index, i), " = new Dictionary<", levelIndexKeyType, ", ", mapType, ">();") + } + x.g.P() + } + } + } +} + +func (x *Generator) genIndexLoader() { + if !x.needGenerateIndex() { + return + } + defer x.genIndexSorter() + x.g.P(helper.Indent(3), "// Index init.") + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.Indexes { + x.g.P(helper.Indent(3), x.indexContainerName(index, 0), ".Clear();") + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(3), x.indexContainerName(index, i), ".Clear();") + } + } + } + parentDataName := "_data" + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + itemName := fmt.Sprintf("item%d", lm.Depth) + if !lm.NeedGenIndex() { + break + } + x.g.P(helper.Indent(lm.Depth+2), "foreach (var ", itemName, " in ", parentDataName, x.fieldGetter(lm.FD), ")") + x.g.P(helper.Indent(lm.Depth+2), "{") + parentDataName = itemName + if lm.FD.IsMap() { + if lm.NeedMapKeyForIndex() { + x.g.P(helper.Indent(lm.Depth+3), "var k", lm.MapDepth, " = ", itemName, ".Key;") + } + parentDataName = itemName + ".Value" + } + defer x.g.P(helper.Indent(lm.Depth+2), "}") + for _, index := range lm.Indexes { + x.genOneCsharpIndexLoader(lm, index, parentDataName) + } + } +} + +func (x *Generator) genOneCsharpIndexLoader(lm *index.LevelMessage, index *index.LevelIndex, parentDataName string) { + ident := lm.Depth + 1 + x.g.P(helper.Indent(ident+2), "{") + x.g.P(helper.Indent(ident+3), "// Index: ", index.Index) + if len(index.ColFields) == 1 { + // single-column index + field := index.ColFields[0] // just take the first field + fieldName, _ := x.parseKeyFieldNameAndSuffix(field) + if field.FD.IsList() { + itemName := fmt.Sprintf("item%d", lm.MapDepth+1) + x.g.P(helper.Indent(ident+3), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")") + x.g.P(helper.Indent(ident+3), "{") + key := itemName + x.genLoader(lm, index, ident+4, key, parentDataName) + x.g.P(helper.Indent(ident+3), "}") + } else { + key := parentDataName + fieldName + x.g.P(helper.Indent(ident+3), "var key = ", key, ";") + x.genLoader(lm, index, ident+3, "key", parentDataName) + } + } else { + // multi-column index + x.generateOneMulticolumnIndex(lm, index, ident+2, parentDataName, nil) + } + x.g.P(helper.Indent(ident+2), "}") +} + +func (x *Generator) generateOneMulticolumnIndex(lm *index.LevelMessage, index *index.LevelIndex, ident int, parentDataName string, keys helper.MapKeySlice) { + cursor := len(keys) + if cursor >= len(index.ColFields) { + keyType := x.indexMapKeyType(index) + x.g.P(helper.Indent(ident+1), "var key = new ", keyType, "(", keys.GenGetArguments(), ");") + x.genLoader(lm, index, ident+1, "key", parentDataName) + return + } + field := index.ColFields[cursor] + fieldName, _ := x.parseKeyFieldNameAndSuffix(field) + if field.FD.IsList() { + itemName := fmt.Sprintf("indexItem%d", cursor) + x.g.P(helper.Indent(ident+1), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")") + x.g.P(helper.Indent(ident+1), "{") + key := itemName + keys = keys.AddMapKey(helper.MapKey{Name: key}) + x.generateOneMulticolumnIndex(lm, index, ident+1, parentDataName, keys) + x.g.P(helper.Indent(ident+1), "}") + } else { + key := parentDataName + fieldName + keys = keys.AddMapKey(helper.MapKey{Name: key}) + x.generateOneMulticolumnIndex(lm, index, ident, parentDataName, keys) + } +} + +func (x *Generator) genLoader(lm *index.LevelMessage, index *index.LevelIndex, ident int, key, parentDataName string) { + valueType := x.mapValueType(index) + x.g.P(helper.Indent(ident), "{") + x.g.P(helper.Indent(ident+1), "var list = ", x.indexContainerName(index, 0), ".TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : ", x.indexContainerName(index, 0), "[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + x.g.P(helper.Indent(ident), "}") + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(ident), "{") + if i == 1 { + x.g.P(helper.Indent(ident+1), "var map = ", x.indexContainerName(index, i), ".TryGetValue(k1, out var existingMap) ?") + x.g.P(helper.Indent(ident+1), "existingMap : ", x.indexContainerName(index, i), "[k1] = new ", x.indexMapType(index), "();") + x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + } else { + var fields []string + for j := 1; j <= i; j++ { + fields = append(fields, fmt.Sprintf("k%d", j)) + } + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(ident+1), "var mapKey = new ", levelIndexKeyType, "(", strings.Join(fields, ", "), ");") + x.g.P(helper.Indent(ident+1), "var map = ", x.indexContainerName(index, i), ".TryGetValue(mapKey, out var existingMap) ?") + x.g.P(helper.Indent(ident+1), "existingMap : ", x.indexContainerName(index, i), "[mapKey] = new ", x.indexMapType(index), "();") + x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + } + x.g.P(helper.Indent(ident), "}") + } +} + +func (x *Generator) genIndexSorter() { + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.Indexes { + if len(index.SortedColFields) != 0 { + valueType := x.mapValueType(index) + x.g.P(helper.Indent(3), "// Index(sort): ", index.Index) + indexContainerName := x.indexContainerName(index, 0) + sorter := strings.TrimPrefix(indexContainerName, "_") + "Comparison" + x.g.P(helper.Indent(3), "Comparison<", valueType, "> ", sorter, " = (a, b) =>") + var keys helper.MapKeySlice + for _, field := range index.SortedColFields { + fieldName, _ := x.parseKeyFieldNameAndSuffix(field) + keys = keys.AddMapKey(helper.MapKey{Name: fieldName}) + } + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return "a" + key.Name }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "b" + key.Name }, ", "), "));") + x.g.P(helper.Indent(3), "foreach (var itemList in ", indexContainerName, ".Values)") + x.g.P(helper.Indent(3), "{") + x.g.P(helper.Indent(4), "itemList.Sort(", sorter, ");") + x.g.P(helper.Indent(3), "}") + // Iterate all leveled containers. + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(3), "foreach (var itemDict in ", x.indexContainerName(index, i), ".Values)") + x.g.P(helper.Indent(3), "{") + x.g.P(helper.Indent(4), "foreach (var itemList in itemDict.Values)") + x.g.P(helper.Indent(4), "{") + x.g.P(helper.Indent(5), "itemList.Sort(", sorter, ");") + x.g.P(helper.Indent(4), "}") + x.g.P(helper.Indent(3), "}") + } + } + } + } +} + +func (x *Generator) genIndexFinders() { + if !x.needGenerateIndex() { + return + } + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.Indexes { + mapType := x.indexMapType(index) + mapValueType := x.mapValueType(index) + indexContainerName := x.indexContainerName(index, 0) + + x.g.P() + x.g.P(helper.Indent(2), "// Index: ", index.Index) + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map finds the index: key(", index.Index, ") to value(", mapValueType, ") map.") + x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ref readonly ", mapType, " Find", index.Name(), "Map() => ref ", indexContainerName, ";") + x.g.P() + + keyType := x.indexMapKeyType(index) + keys := x.indexKeys(index) + params := keys.GenGetParams() + args := keys.GenGetArguments() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), " finds a list of all values of the given key(s).") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), "(", params, ") =>") + if len(index.ColFields) == 1 { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", args, ", out var value) ? value : null;") + } else { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", keyType, "(", args, "), out var value) ? value : null;") + } + x.g.P() + + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), " finds the first value of the given key(s),") + x.g.P(helper.Indent(2), "/// or null if no value found.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), "(", params, ") =>") + x.g.P(helper.Indent(3), "Find", index.Name(), "(", args, ")?.FirstOrDefault();") + + for i := 1; i < lm.MapDepth; i++ { + indexContainerName := x.indexContainerName(index, i) + partKeys := x.keys[:i] + partParams := partKeys.GenGetParams() + partArgs := partKeys.GenGetArguments() + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map", i, " finds the index: key(", index.Index, ") to value(", mapValueType, "),") + x.g.P(helper.Indent(2), "/// which is the upper ", loadutil.Ordinal(i), "-level map specified by (", partArgs, ").") + x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapType, "? Find", index.Name(), "Map", i, "(", partParams, ") =>") + if len(partKeys) == 1 { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", partArgs, ", out var value) ? value : null;") + } else { + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", levelIndexKeyType, "(", partArgs, "), out var value) ? value : null;") + } + + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), i, " finds a list of all values of the given key(s) in the upper ", loadutil.Ordinal(i), "-level map") + x.g.P(helper.Indent(2), "/// specified by (", partArgs, ").") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), i, "(", partParams, ", ", params, ") =>") + if len(index.ColFields) == 1 { + x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(", args, ", out var value) == true ? value : null;") + } else { + x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(new ", keyType, "(", args, "), out var value) == true ? value : null;") + } + + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), i, " finds the first value of the given key(s) in the upper ", loadutil.Ordinal(i), "-level map") + x.g.P(helper.Indent(2), "/// specified by (", partArgs, "), or null if no value found.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), i, "(", partParams, ", ", params, ") =>") + x.g.P(helper.Indent(3), "Find", index.Name(), i, "(", partArgs, ", ", args, ")?.FirstOrDefault();") + } + } + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go b/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go new file mode 100644 index 00000000..d139a283 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/indexes/ordered_index.go @@ -0,0 +1,341 @@ +package indexes + +import ( + "fmt" + "strings" + "sync" + + "github.com/iancoleman/strcase" + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/internal/index" + "github.com/tableauio/loader/internal/loadutil" + "github.com/tableauio/loader/internal/options" +) + +func (x *Generator) needGenerateOrderedIndex() bool { + return options.NeedGenOrderedIndex(x.message.Desc, options.LangCS) +} + +func (x *Generator) orderedIndexMapType(index *index.LevelIndex) string { + return fmt.Sprintf("OrderedIndex_%sMap", index.Name()) +} + +func (x *Generator) orderedIndexMapKeyType(index *index.LevelIndex) string { + if len(index.ColFields) == 1 { + // single-column index + field := index.ColFields[0] // just take first field + return helper.ParseOrderedIndexKeyType(field.FD) + } else { + // multi-column index + return fmt.Sprintf("OrderedIndex_%sKey", index.Name()) + } +} + +func (x *Generator) orderedIndexContainerName(index *index.LevelIndex, i int) string { + if i == 0 { + return fmt.Sprintf("_orderedIndex%sMap", strcase.ToCamel(index.Name())) + } + return fmt.Sprintf("_orderedIndex%sMap%d", strcase.ToCamel(index.Name()), i) +} + +func (x *Generator) orderedIndexKeys(index *index.LevelIndex) helper.MapKeySlice { + var keys helper.MapKeySlice + for _, field := range index.ColFields { + keys = keys.AddMapKey(helper.MapKey{ + Type: helper.ParseOrderedIndexKeyType(field.FD), + Name: helper.ParseIndexFieldNameAsFuncParam(field.FD), + FieldName: helper.ParseIndexFieldNameAsKeyStructFieldName(field.FD), + }) + } + return keys +} + +func (x *Generator) genOrderedIndexTypeDef() { + if !x.needGenerateOrderedIndex() { + return + } + var once sync.Once + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.OrderedIndexes { + once.Do(func() { x.g.P(helper.Indent(2), "// OrderedIndex types.") }) + x.g.P(helper.Indent(2), "// OrderedIndex: ", index.Index) + mapType := x.orderedIndexMapType(index) + keyType := x.orderedIndexMapKeyType(index) + valueType := x.mapValueType(index) + keys := x.orderedIndexKeys(index) + if len(index.ColFields) != 1 { + // Generate key struct + x.g.P(helper.Indent(2), "public readonly struct ", keyType, " : IComparable<", keyType, ">") + x.g.P(helper.Indent(2), "{") + for _, key := range keys { + x.g.P(helper.Indent(3), "public ", key.Type, " ", key.FieldName, " { get; }") + } + x.g.P() + x.g.P(helper.Indent(3), "public ", keyType, "(", keys.GenGetParams(), ")") + x.g.P(helper.Indent(3), "{") + for _, key := range keys { + x.g.P(helper.Indent(4), key.FieldName, " = ", key.Name, ";") + } + x.g.P(helper.Indent(3), "}") + x.g.P() + x.g.P(helper.Indent(3), "public int CompareTo(", keyType, " other) =>") + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return key.FieldName }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "other." + key.FieldName }, ", "), "));") + x.g.P(helper.Indent(2), "}") + x.g.P() + } + x.g.P(helper.Indent(2), "public class ", mapType, " : SortedDictionary<", keyType, ", List<", valueType, ">> { }") + x.g.P() + + x.g.P(helper.Indent(2), "private ", mapType, " ", x.orderedIndexContainerName(index, 0), " = new ", mapType, "();") + x.g.P() + for i := 1; i < lm.MapDepth; i++ { + if i == 1 { + x.g.P(helper.Indent(2), "private Dictionary<", x.keys[0].Type, ", ", mapType, "> ", x.orderedIndexContainerName(index, i), " = new Dictionary<", x.keys[0].Type, ", ", mapType, ">();") + } else { + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(2), "private Dictionary<", levelIndexKeyType, ", ", mapType, "> ", x.orderedIndexContainerName(index, i), " = new Dictionary<", levelIndexKeyType, ", ", mapType, ">();") + } + x.g.P() + } + } + } +} + +func (x *Generator) genOrderedIndexLoader() { + if !x.needGenerateOrderedIndex() { + return + } + defer x.genOrderedIndexSorter() + x.g.P(helper.Indent(3), "// OrderedIndex init.") + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.OrderedIndexes { + x.g.P(helper.Indent(3), x.orderedIndexContainerName(index, 0), ".Clear();") + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(3), x.orderedIndexContainerName(index, i), ".Clear();") + } + } + } + parentDataName := "_data" + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + itemName := fmt.Sprintf("item%d", lm.Depth) + if !lm.NeedGenOrderedIndex() { + break + } + x.g.P(helper.Indent(lm.Depth+2), "foreach (var ", itemName, " in ", parentDataName, x.fieldGetter(lm.FD), ")") + x.g.P(helper.Indent(lm.Depth+2), "{") + parentDataName = itemName + if lm.FD.IsMap() { + if lm.NeedMapKeyForOrderedIndex() { + x.g.P(helper.Indent(lm.Depth+3), "var k", lm.MapDepth, " = ", itemName, ".Key;") + } + parentDataName = itemName + ".Value" + } + defer x.g.P(helper.Indent(lm.Depth+2), "}") + for _, index := range lm.OrderedIndexes { + x.genOneCsharpOrderedIndexLoader(lm, index, parentDataName) + } + } +} + +func (x *Generator) genOneCsharpOrderedIndexLoader(lm *index.LevelMessage, index *index.LevelIndex, parentDataName string) { + ident := lm.Depth + 1 + x.g.P(helper.Indent(ident+2), "{") + x.g.P(helper.Indent(ident+3), "// OrderedIndex: ", index.Index) + if len(index.ColFields) == 1 { + // single-column index + field := index.ColFields[0] // just take the first field + fieldName, suffix := x.parseKeyFieldNameAndSuffix(field) + if field.FD.IsList() { + itemName := fmt.Sprintf("item%d", lm.MapDepth+1) + x.g.P(helper.Indent(ident+3), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")") + x.g.P(helper.Indent(ident+3), "{") + x.g.P(helper.Indent(ident+4), "var key = ", itemName, suffix, ";") + x.genOrderedIndexLoaderCommon(lm, index, ident+4, "key", parentDataName) + x.g.P(helper.Indent(ident+3), "}") + } else { + key := parentDataName + fieldName + suffix + x.g.P(helper.Indent(ident+3), "var key = ", key, ";") + x.genOrderedIndexLoaderCommon(lm, index, ident+3, "key", parentDataName) + } + } else { + // multi-column index + x.generateOneMulticolumnOrderedIndex(lm, index, ident+2, parentDataName, nil) + } + x.g.P(helper.Indent(ident+2), "}") +} + +func (x *Generator) generateOneMulticolumnOrderedIndex(lm *index.LevelMessage, index *index.LevelIndex, ident int, parentDataName string, keys helper.MapKeySlice) { + cursor := len(keys) + if cursor >= len(index.ColFields) { + keyType := x.orderedIndexMapKeyType(index) + x.g.P(helper.Indent(ident+1), "var key = new ", keyType, "(", keys.GenGetArguments(), ");") + x.genOrderedIndexLoaderCommon(lm, index, ident+1, "key", parentDataName) + return + } + field := index.ColFields[cursor] + fieldName, suffix := x.parseKeyFieldNameAndSuffix(field) + if field.FD.IsList() { + itemName := fmt.Sprintf("indexItem%d", cursor) + x.g.P(helper.Indent(ident+1), "foreach (var ", itemName, " in ", parentDataName, fieldName, ")") + x.g.P(helper.Indent(ident+1), "{") + key := itemName + keys = keys.AddMapKey(helper.MapKey{Name: key}) + x.generateOneMulticolumnOrderedIndex(lm, index, ident+1, parentDataName, keys) + x.g.P(helper.Indent(ident+1), "}") + } else { + key := parentDataName + fieldName + suffix + keys = keys.AddMapKey(helper.MapKey{Name: key}) + x.generateOneMulticolumnOrderedIndex(lm, index, ident, parentDataName, keys) + } +} + +func (x *Generator) genOrderedIndexLoaderCommon(lm *index.LevelMessage, index *index.LevelIndex, ident int, key, parentDataName string) { + valueType := x.mapValueType(index) + x.g.P(helper.Indent(ident), "{") + x.g.P(helper.Indent(ident+1), "var list = ", x.orderedIndexContainerName(index, 0), ".TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : ", x.orderedIndexContainerName(index, 0), "[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + x.g.P(helper.Indent(ident), "}") + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(ident), "{") + if i == 1 { + x.g.P(helper.Indent(ident+1), "var map = ", x.orderedIndexContainerName(index, i), ".TryGetValue(k1, out var existingMap) ?") + x.g.P(helper.Indent(ident+1), "existingMap : ", x.orderedIndexContainerName(index, i), "[k1] = new ", x.orderedIndexMapType(index), "();") + x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + } else { + var fields []string + for j := 1; j <= i; j++ { + fields = append(fields, fmt.Sprintf("k%d", j)) + } + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(ident+1), "var mapKey = new ", levelIndexKeyType, "(", strings.Join(fields, ", "), ");") + x.g.P(helper.Indent(ident+1), "var map = ", x.orderedIndexContainerName(index, i), ".TryGetValue(mapKey, out var existingMap) ?") + x.g.P(helper.Indent(ident+1), "existingMap : ", x.orderedIndexContainerName(index, i), "[mapKey] = new ", x.orderedIndexMapType(index), "();") + x.g.P(helper.Indent(ident+1), "var list = map.TryGetValue(", key, ", out var existingList) ?") + x.g.P(helper.Indent(ident+1), "existingList : map[", key, "] = new List<", valueType, ">();") + x.g.P(helper.Indent(ident+1), "list.Add(", parentDataName, ");") + } + x.g.P(helper.Indent(ident), "}") + } +} + +func (x *Generator) genOrderedIndexSorter() { + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.OrderedIndexes { + if len(index.SortedColFields) != 0 { + valueType := x.mapValueType(index) + x.g.P(helper.Indent(3), "// OrderedIndex(sort): ", index.Index) + indexContainerName := x.orderedIndexContainerName(index, 0) + sorter := strings.TrimPrefix(indexContainerName, "_") + "Comparison" + x.g.P(helper.Indent(3), "Comparison<", valueType, "> ", sorter, " = (a, b) =>") + var keys helper.MapKeySlice + for _, field := range index.SortedColFields { + fieldName, _ := x.parseKeyFieldNameAndSuffix(field) + keys = keys.AddMapKey(helper.MapKey{Name: fieldName}) + } + x.g.P(helper.Indent(4), "(", keys.GenCustom(func(key helper.MapKey) string { return "a" + key.Name }, ", "), ").CompareTo((", keys.GenCustom(func(key helper.MapKey) string { return "b" + key.Name }, ", "), "));") + x.g.P(helper.Indent(3), "foreach (var itemList in ", indexContainerName, ".Values)") + x.g.P(helper.Indent(3), "{") + x.g.P(helper.Indent(4), "itemList.Sort(", sorter, ");") + x.g.P(helper.Indent(3), "}") + // Iterate all leveled containers. + for i := 1; i < lm.MapDepth; i++ { + x.g.P(helper.Indent(3), "foreach (var itemDict in ", x.orderedIndexContainerName(index, i), ".Values)") + x.g.P(helper.Indent(3), "{") + x.g.P(helper.Indent(4), "foreach (var itemList in itemDict.Values)") + x.g.P(helper.Indent(4), "{") + x.g.P(helper.Indent(5), "itemList.Sort(", sorter, ");") + x.g.P(helper.Indent(4), "}") + x.g.P(helper.Indent(3), "}") + } + } + } + } +} + +func (x *Generator) genOrderedIndexFinders() { + if !x.needGenerateOrderedIndex() { + return + } + for lm := x.descriptor.LevelMessage; lm != nil; lm = lm.NextLevel { + for _, index := range lm.OrderedIndexes { + mapType := x.orderedIndexMapType(index) + mapValueType := x.mapValueType(index) + indexContainerName := x.orderedIndexContainerName(index, 0) + + x.g.P() + x.g.P(helper.Indent(2), "// OrderedIndex: ", index.Index) + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map finds the ordered index: key(", index.Index, ") to value(", mapValueType, ") sorted map.") + x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ref readonly ", mapType, " Find", index.Name(), "Map() => ref ", indexContainerName, ";") + x.g.P() + + keyType := x.orderedIndexMapKeyType(index) + keys := x.orderedIndexKeys(index) + params := keys.GenGetParams() + args := keys.GenGetArguments() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), " finds a list of all values of the given key(s).") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), "(", params, ") =>") + if len(index.ColFields) == 1 { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", args, ", out var value) ? value : null;") + } else { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", keyType, "(", args, "), out var value) ? value : null;") + } + x.g.P() + + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), " finds the first value of the given key(s),") + x.g.P(helper.Indent(2), "/// or null if no value found.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), "(", params, ") =>") + x.g.P(helper.Indent(3), "Find", index.Name(), "(", args, ")?.FirstOrDefault();") + + for i := 1; i < lm.MapDepth; i++ { + indexContainerName := x.orderedIndexContainerName(index, i) + partKeys := x.keys[:i] + partParams := partKeys.GenGetParams() + partArgs := partKeys.GenGetArguments() + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), "Map", i, " finds the ordered index: key(", index.Index, ") to value(", mapValueType, "),") + x.g.P(helper.Indent(2), "/// which is the upper ", loadutil.Ordinal(i), "-level sorted map specified by (", partArgs, ").") + x.g.P(helper.Indent(2), "/// One key may correspond to multiple values, which are represented by a list.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapType, "? Find", index.Name(), "Map", i, "(", partParams, ") =>") + if len(partKeys) == 1 { + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(", partArgs, ", out var value) ? value : null;") + } else { + levelIndexKeyType := x.levelKeyType(x.keys[i-1].Fd) + x.g.P(helper.Indent(3), indexContainerName, ".TryGetValue(new ", levelIndexKeyType, "(", partArgs, "), out var value) ? value : null;") + } + + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// Find", index.Name(), i, " finds a list of all values of the given key(s) in the upper ", loadutil.Ordinal(i), "-level sorted map") + x.g.P(helper.Indent(2), "/// specified by (", partArgs, ").") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public List<", mapValueType, ">? Find", index.Name(), i, "(", partParams, ", ", params, ") =>") + if len(index.ColFields) == 1 { + x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(", args, ", out var value) == true ? value : null;") + } else { + x.g.P(helper.Indent(3), "Find", index.Name(), "Map", i, "(", partArgs, ")?.TryGetValue(new ", keyType, "(", args, "), out var value) == true ? value : null;") + } + + x.g.P() + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// FindFirst", index.Name(), i, " finds the first value of the given key(s) in the upper ", loadutil.Ordinal(i), "-level sorted map") + x.g.P(helper.Indent(2), "/// specified by (", partArgs, "), or null if no value found.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ", mapValueType, "? FindFirst", index.Name(), i, "(", partParams, ", ", params, ") =>") + x.g.P(helper.Indent(3), "Find", index.Name(), i, "(", partArgs, ", ", args, ")?.FirstOrDefault();") + } + } + } +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/main.go b/cmd/protoc-gen-csharp-tableau-loader/main.go new file mode 100644 index 00000000..bad35e59 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "flag" + "fmt" + + "github.com/tableauio/loader/internal/options" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/types/pluginpb" +) + +const version = "0.1.0" + +func main() { + showVersion := flag.Bool("version", false, "print the version and exit") + flag.Parse() + if *showVersion { + fmt.Printf("protoc-gen-csharp-tableau-loader %v\n", version) + return + } + + var flags flag.FlagSet + + protogen.Options{ + ParamFunc: flags.Set, + }.Run(func(gen *protogen.Plugin) error { + gen.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + for _, f := range gen.Files { + if !options.NeedGenFile(f) { + continue + } + generateMessager(gen, f) + } + generateHub(gen) + generateEmbed(gen) + return nil + }) +} diff --git a/cmd/protoc-gen-csharp-tableau-loader/messager.go b/cmd/protoc-gen-csharp-tableau-loader/messager.go new file mode 100644 index 00000000..f38cadcb --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/messager.go @@ -0,0 +1,176 @@ +package main + +import ( + "fmt" + "path/filepath" + + "github.com/iancoleman/strcase" + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/indexes" + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/orderedmap" + "github.com/tableauio/loader/internal/extensions" + "github.com/tableauio/loader/internal/index" + "github.com/tableauio/loader/internal/loadutil" + "github.com/tableauio/tableau/proto/tableaupb" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +// golbal container for record all proto filenames and messager names +var messagers []string + +// generateMessager generates a protoconf file corresponding to the protobuf file. +// Each wrapped struct type implement the Messager interface. +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 generates struct type definitions. +func generateFileContent(gen *protogen.Plugin, file *protogen.File, g *protogen.GeneratedFile) { + g.P(staticMessagerContent1) + var fileMessagers []string + 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 !firstMessager { + g.P() + } + firstMessager = false + genMessage(gen, g, message) + + messagerName := string(message.Desc.Name()) + fileMessagers = append(fileMessagers, messagerName) + } + } + messagers = append(messagers, fileMessagers...) + g.P(staticMessagerContent2) +} + +// genMessage generates a message definition. +func genMessage(gen *protogen.Plugin, g *protogen.GeneratedFile, message *protogen.Message) { + messagerName := string(message.Desc.Name()) + indexDescriptor := index.ParseIndexDescriptor(message.Desc) + + orderedMapGenerator := orderedmap.NewGenerator(g, message) + indexGenerator := indexes.NewGenerator(g, indexDescriptor, message) + + g.P(helper.Indent(1), "/// ") + g.P(helper.Indent(1), "/// ", messagerName, " is a wrapper around protobuf message ", helper.ParseCsharpClassType(message.Desc), ".") + g.P(helper.Indent(1), "/// ") + g.P(helper.Indent(1), "public class ", messagerName, " : Messager, IMessagerName") + g.P(helper.Indent(1), "{") + // type definitions + orderedMapGenerator.GenOrderedMapTypeDef() + indexGenerator.GenIndexTypeDef() + g.P(helper.Indent(2), "private ", helper.ParseCsharpClassType(message.Desc), " _data = new();") + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// Name returns the ", messagerName, "'s message name.") + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "public string Name() => ", helper.ParseCsharpClassType(message.Desc), ".Descriptor.Name;") + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// Load loads ", messagerName, "'s content in the given dir, based on format and messager options.") + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null)") + g.P(helper.Indent(2), "{") + g.P(helper.Indent(3), "var start = DateTime.Now;") + g.P(helper.Indent(3), "try") + g.P(helper.Indent(3), "{") + g.P(helper.Indent(4), "_data = (", helper.ParseCsharpClassType(message.Desc), ")(") + g.P(helper.Indent(5), "Tableau.Load.LoadMessagerInDir(", helper.ParseCsharpClassType(message.Desc), ".Descriptor, dir, fmt, options)") + g.P(helper.Indent(5), "?? throw new InvalidOperationException()") + g.P(helper.Indent(4), ");") + g.P(helper.Indent(3), "}") + g.P(helper.Indent(3), "catch (Exception ex)") + g.P(helper.Indent(3), "{") + g.P(helper.Indent(4), "if (string.IsNullOrEmpty(Util.GetErrMsg()))") + g.P(helper.Indent(4), "{") + g.P(helper.Indent(5), "Util.SetErrMsg($\"failed to load ", messagerName, ": {ex.Message}\");") + g.P(helper.Indent(4), "}") + g.P(helper.Indent(4), "return false;") + g.P(helper.Indent(3), "}") + g.P(helper.Indent(3), "LoadStats.Duration = DateTime.Now - start;") + g.P(helper.Indent(3), "return ProcessAfterLoad();") + g.P(helper.Indent(2), "}") + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// Data returns the ", messagerName, "'s inner message data.") + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "public ref readonly ", helper.ParseCsharpClassType(message.Desc), " Data() => ref _data;") + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// Message returns the ", messagerName, "'s inner message data.") + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "public override pb::IMessage? Message() => _data;") + + if orderedMapGenerator.NeedGenerate() || indexGenerator.NeedGenerate() { + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// ProcessAfterLoad runs after this messager is loaded.") + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "protected override bool ProcessAfterLoad()") + g.P(helper.Indent(2), "{") + orderedMapGenerator.GenOrderedMapLoader() + indexGenerator.GenIndexLoader() + g.P(helper.Indent(3), "return true;") + g.P(helper.Indent(2), "}") + } + + // syntactic sugar for accessing map items + genMapGetters(gen, g, message.Desc, 1, nil, messagerName) + 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) { + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if fd.IsMap() { + keys = keys.AddMapKey(helper.MapKey{ + Type: helper.ParseMapKeyType(fd.MapKey()), + Name: helper.ParseMapFieldNameAsFuncParam(fd), + }) + getter := fmt.Sprintf("Get%v", depth) + g.P() + g.P(helper.Indent(2), "/// ") + g.P(helper.Indent(2), "/// ", getter, " finds value in the ", loadutil.Ordinal(depth), "-level map.") + g.P(helper.Indent(2), "/// It will return null if the key is not found.") + g.P(helper.Indent(2), "/// ") + + lastKeyName := keys[len(keys)-1].Name + if depth == 1 { + g.P(helper.Indent(2), "public ", helper.ParseMapValueType(fd), "? ", getter, "(", keys.GenGetParams(), ") =>") + g.P(helper.Indent(3), "_data.", helper.ParseCsharpPropertyName(fd), "?.TryGetValue(", lastKeyName, ", out var val) == true ? val : null;") + } else { + prevKeys := keys[:len(keys)-1] + prevGetter := fmt.Sprintf("Get%v", depth-1) + g.P(helper.Indent(2), "public ", helper.ParseMapValueType(fd), "? ", getter, "(", keys.GenGetParams(), ") =>") + g.P(helper.Indent(3), prevGetter, "(", prevKeys.GenGetArguments(), ")?.", helper.ParseCsharpPropertyName(fd), "?.TryGetValue(", lastKeyName, ", out var val) == true ? val : null;") + } + + if fd.MapValue().Kind() == protoreflect.MessageKind { + genMapGetters(gen, g, fd.MapValue().Message(), depth+1, keys, messagerName) + } + break + } + } +} + +const staticMessagerContent1 = `using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{` + +const staticMessagerContent2 = `}` diff --git a/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go b/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go new file mode 100644 index 00000000..048f7b43 --- /dev/null +++ b/cmd/protoc-gen-csharp-tableau-loader/orderedmap/ordered_map.go @@ -0,0 +1,207 @@ +package orderedmap + +import ( + "fmt" + + "github.com/tableauio/loader/cmd/protoc-gen-csharp-tableau-loader/helper" + "github.com/tableauio/loader/internal/loadutil" + "github.com/tableauio/loader/internal/options" + "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/reflect/protoreflect" +) + +type Generator struct { + g *protogen.GeneratedFile + message *protogen.Message +} + +func NewGenerator(g *protogen.GeneratedFile, message *protogen.Message) *Generator { + return &Generator{ + g: g, + message: message, + } +} + +func (x *Generator) NeedGenerate() bool { + return options.NeedGenOrderedMap(x.message.Desc, options.LangCS) +} + +func (x *Generator) mapType(mapFd protoreflect.FieldDescriptor) string { + return fmt.Sprintf("OrderedMap_%sMap", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd)) +} + +func (x *Generator) mapValueType(mapFd protoreflect.FieldDescriptor) string { + return fmt.Sprintf("OrderedMap_%sValue", helper.ParseLeveledMapPrefix(x.message.Desc, mapFd)) +} + +func (x *Generator) mapValueFieldType(fd protoreflect.FieldDescriptor) string { + nextMapFD := getNextLevelMapFD(fd.MapValue()) + if nextMapFD != nil { + return x.mapValueType(fd) + } + return helper.ParseMapValueType(fd) +} + +func (x *Generator) GenOrderedMapTypeDef() { + if !x.NeedGenerate() { + return + } + x.genOrderedMapTypeDef(x.message.Desc, 1, nil) +} + +func (x *Generator) genOrderedMapTypeDef(md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice) { + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if fd.IsMap() { + if depth == 1 { + x.g.P(helper.Indent(2), "// OrderedMap types.") + } + nextKeys := keys.AddMapKey(helper.MapKey{ + Type: helper.ParseMapKeyType(fd.MapKey()), + Name: helper.ParseMapFieldNameAsKeyStructFieldName(fd), + }) + keyType := nextKeys[len(nextKeys)-1].Type + if fd.MapValue().Kind() == protoreflect.MessageKind { + x.genOrderedMapTypeDef(fd.MapValue().Message(), depth+1, nextKeys) + } + orderedMap := x.mapType(fd) + orderedMapValue := x.mapValueType(fd) + nextMapFD := getNextLevelMapFD(fd.MapValue()) + if nextMapFD != nil { + currValueType := helper.ParseCsharpType(fd.MapValue()) + nextOrderedMap := x.mapType(nextMapFD) + x.g.P(helper.Indent(2), "public class ", orderedMapValue, " : Tuple<", nextOrderedMap, ", ", currValueType, ">") + x.g.P(helper.Indent(2), "{") + x.g.P(helper.Indent(3), "public ", orderedMapValue, "(", nextOrderedMap, " item1, ", currValueType, " item2) : base(item1, item2) { }") + x.g.P(helper.Indent(2), "}") + } + x.g.P(helper.Indent(2), "public class ", orderedMap, " : SortedDictionary<", keyType, ", ", x.mapValueFieldType(fd), "> { }") + x.g.P() + if depth == 1 { + x.g.P(helper.Indent(2), "private ", orderedMap, " _orderedMap = new ", orderedMap, "();") + x.g.P() + } + break + } + } +} + +func (x *Generator) GenOrderedMapLoader() { + if !x.NeedGenerate() { + return + } + x.genOrderedMapLoader(x.message.Desc, 1) +} + +func (x *Generator) genOrderedMapLoader(md protoreflect.MessageDescriptor, depth int) { + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if fd.IsMap() { + if depth == 1 { + x.g.P(helper.Indent(3), "// OrderedMap init.") + x.g.P(helper.Indent(3), "_orderedMap.Clear();") + } + orderedMapValue := x.mapValueType(fd) + keyName := fmt.Sprintf("key%d", depth) + valueName := fmt.Sprintf("value%d", depth) + + tmpOrderedMapName := fmt.Sprintf("ordered_map%d", depth) + + prevContainer := fmt.Sprintf("value%d", depth-1) + prevTmpOrderedMapName := fmt.Sprintf("ordered_map%d", depth-1) + if depth == 1 { + prevContainer = "_data" + prevTmpOrderedMapName = "_orderedMap" + } + x.g.P(helper.Indent(depth+2), "foreach (var (", keyName, ", ", valueName, ") in ", prevContainer, ".", helper.ParseCsharpPropertyName(fd), ")") + x.g.P(helper.Indent(depth+2), "{") + nextMapFD := getNextLevelMapFD(fd.MapValue()) + if nextMapFD != nil { + nextOrderedMap := x.mapType(nextMapFD) + x.g.P(helper.Indent(depth+3), "var ", tmpOrderedMapName, " = new ", nextOrderedMap, "();") + } + if fd.MapValue().Kind() == protoreflect.MessageKind { + x.genOrderedMapLoader(fd.MapValue().Message(), depth+1) + } + + if nextMapFD != nil { + x.g.P(helper.Indent(depth+3), prevTmpOrderedMapName, "[", keyName, "] = new ", orderedMapValue, "(", tmpOrderedMapName, ", ", valueName, ");") + } else { + x.g.P(helper.Indent(depth+3), prevTmpOrderedMapName, "[", keyName, "] = ", valueName, ";") + } + x.g.P(helper.Indent(depth+2), "}") + break + } + } +} + +func (x *Generator) GenOrderedMapGetters() { + if !x.NeedGenerate() { + return + } + x.genOrderedMapGetters(x.message.Desc, 1, nil) +} + +func (x *Generator) genOrderedMapGetters(md protoreflect.MessageDescriptor, depth int, keys helper.MapKeySlice) { + genGetterName := func(depth int) string { + getter := "GetOrderedMap" + if depth > 1 { + getter = fmt.Sprintf("GetOrderedMap%v", depth-1) + } + return getter + } + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if fd.IsMap() { + x.g.P() + if depth == 1 { + x.g.P(helper.Indent(2), "// OrderedMap accessers.") + } + getter := genGetterName(depth) + orderedMap := x.mapType(fd) + if depth == 1 { + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// ", getter, " returns the ", loadutil.Ordinal(depth), "-level ordered map.") + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "public ref readonly ", orderedMap, " ", getter, "() => ref _orderedMap;") + } else { + x.g.P(helper.Indent(2), "/// ") + x.g.P(helper.Indent(2), "/// ", getter, " finds value in the ", loadutil.Ordinal(depth-1), "-level ordered map.") + x.g.P(helper.Indent(2), "/// It will return null if the key is not found.") + x.g.P(helper.Indent(2), "/// ") + lastKeyName := keys[len(keys)-1].Name + if depth == 2 { + x.g.P(helper.Indent(2), "public ", orderedMap, "? ", getter, "(", keys.GenGetParams(), ") =>") + x.g.P(helper.Indent(3), "_orderedMap.TryGetValue(", lastKeyName, ", out var value) ? value.Item1 : null;") + } else { + prevKeys := keys[:len(keys)-1] + prevGetter := genGetterName(depth - 1) + x.g.P(helper.Indent(2), "public ", orderedMap, "? ", getter, "(", keys.GenGetParams(), ") =>") + x.g.P(helper.Indent(3), prevGetter, "(", prevKeys.GenGetArguments(), ")?.TryGetValue(", lastKeyName, ", out var value) == true ? value.Item1 : null;") + } + } + + nextKeys := keys.AddMapKey(helper.MapKey{ + Type: helper.ParseMapKeyType(fd.MapKey()), + Name: helper.ParseMapFieldNameAsFuncParam(fd), + }) + if fd.MapValue().Kind() == protoreflect.MessageKind { + x.genOrderedMapGetters(fd.MapValue().Message(), depth+1, nextKeys) + } + break + } + } +} + +func getNextLevelMapFD(fd protoreflect.FieldDescriptor) protoreflect.FieldDescriptor { + if fd.Kind() == protoreflect.MessageKind { + md := fd.Message() + for i := 0; i < md.Fields().Len(); i++ { + fd := md.Fields().Get(i) + if fd.IsMap() { + return fd + } + } + } + return nil +} diff --git a/internal/options/options.go b/internal/options/options.go index c56d5b12..df5f801a 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -22,6 +22,7 @@ type Language = string const ( LangCPP Language = "cpp" LangGO Language = "go" + LangCS Language = "cs" ) func NeedGenOrderedMap(md protoreflect.MessageDescriptor, lang Language) bool { diff --git a/test/csharp-tableau-loader/Loader.csproj b/test/csharp-tableau-loader/Loader.csproj new file mode 100644 index 00000000..68dbf5e1 --- /dev/null +++ b/test/csharp-tableau-loader/Loader.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + disable + enable + CS8981 + + 9 + + + + + + + diff --git a/test/csharp-tableau-loader/Program.cs b/test/csharp-tableau-loader/Program.cs new file mode 100644 index 00000000..63f85cdd --- /dev/null +++ b/test/csharp-tableau-loader/Program.cs @@ -0,0 +1,145 @@ +using System; + +class Program +{ + static void Main(string[] _) + { + Tableau.Registry.Init(); + + var options = new Tableau.HubOptions + { + Filter = name => name != "TaskConf" + }; + var hub = new Tableau.Hub(options); + var loadOptions = new Tableau.Load.Options + { + IgnoreUnknownFields = true + }; + if (!hub.Load("../testdata/conf", Tableau.Format.JSON, loadOptions)) + { + Console.WriteLine("Failed to load configurations"); + return; + } + + var activityConf = hub.GetActivityConf(); + if (activityConf is null) + { + Console.WriteLine("ActivityConf is null"); + } + else + { + // error: not found + var notFound = activityConf.Get3(100001, 1, 999); + if (notFound is null) + { + Console.WriteLine("error: not found: ActivityConf.Get3(100001, 1, 999)"); + } + + // get section + var section = activityConf.Get3(100001, 1, 2); + if (section != null) + { + Console.WriteLine($"ActivityConf.Get3(100001, 1, 2): {section}"); + } + + // OrderedMap traversal + var activityOrderedMap = activityConf.GetOrderedMap(); + foreach (var activityPair in activityOrderedMap) + { + Console.WriteLine($"activityId: {activityPair.Key}"); + Console.WriteLine($" - Activity Data: {activityPair.Value.Item2}"); + var chapterOrderedMap = activityPair.Value.Item1; + foreach (var chapterPair in chapterOrderedMap) + { + Console.WriteLine($" chapterId: {chapterPair.Key}"); + Console.WriteLine($" - Chapter Data: {chapterPair.Value.Item2}"); + } + } + } + + var taskConf = hub.Get(); + if (taskConf is null) + { + Console.WriteLine("TaskConf is null"); + } + else + { + Console.WriteLine($"TaskConf: {taskConf.Data()}"); + Console.WriteLine($"TaskConf Load duration: {taskConf.GetStats().Duration.TotalMilliseconds} ms"); + } + + var heroConf = hub.Get(); + if (heroConf is null) + { + Console.WriteLine("HeroConf is null"); + } + else + { + Console.WriteLine($"HeroConf: {heroConf.Data()}"); + Console.WriteLine($"HeroConf Load duration: {heroConf.GetStats().Duration.TotalMilliseconds} ms"); + // Traverse top-level OrderedMap (HeroOrderedMap) + var heroOrderedMap = heroConf.GetOrderedMap(); + if (heroOrderedMap != null) + { + Console.WriteLine("Hero OrderedMap:"); + foreach (var heroPair in heroOrderedMap) + { + Console.WriteLine($"Hero: {heroPair.Key}"); + Console.WriteLine($" - Hero Data: {heroPair.Value.Item2}"); + // Traverse nested Attr OrderedMap + var attrOrderedMap = heroPair.Value.Item1; + if (attrOrderedMap != null && attrOrderedMap.Count > 0) + { + Console.WriteLine(" Attributes:"); + foreach (var attrPair in attrOrderedMap) + { + Console.WriteLine($" - {attrPair.Key}: {attrPair.Value}"); + } + } + } + } + } + + var itemConf = hub.Get(); + if (itemConf is null) + { + Console.WriteLine("ItemConf is null"); + } + else + { + Console.WriteLine($"ItemConf: {itemConf.Data()}"); + Console.WriteLine($"ItemConf Load duration: {itemConf.GetStats().Duration.TotalMilliseconds} ms"); + var itemConf2 = hub.GetItemConf(); + Console.WriteLine($"hub.Get() returns same instance with hub.GetItemConf(): {ReferenceEquals(itemConf, itemConf2)}"); + var itemInfoMap = itemConf.FindItemInfoMap(); + if (itemInfoMap != null) + { + Console.WriteLine("ItemInfoMap Contents:"); + foreach (var itemPair in itemInfoMap) + { + Console.WriteLine($" - {itemPair.Key}: "); + foreach (var element in itemPair.Value) + { + Console.WriteLine($" - {element}"); + } + } + } + } + + LoadBin(); + } + + static void LoadBin() + { + Console.WriteLine("LoadBin"); + var heroConf = new Tableau.HeroConf(); + if (heroConf.Load("../testdata/bin", Tableau.Format.Bin)) + { + Console.WriteLine($"HeroConf: {heroConf.Data()}"); + } + if (!heroConf.Load("../testdata/notexist", Tableau.Format.Bin)) + { + Console.WriteLine("HeroConf not exist"); + } + } +} \ No newline at end of file diff --git a/test/csharp-tableau-loader/gen.bat b/test/csharp-tableau-loader/gen.bat new file mode 100644 index 00000000..3a3d50f7 --- /dev/null +++ b/test/csharp-tableau-loader/gen.bat @@ -0,0 +1,83 @@ +@echo off +setlocal +setlocal enabledelayedexpansion + +for /f "delims=" %%i in ('git rev-parse --show-toplevel') do set repoRoot=%%i +cd /d "%repoRoot%" + +REM Allow overriding protoc via environment variable. +REM Default to locally compiled protoc for local development; fallback to system protoc. +if not defined PROTOC ( + if exist "%repoRoot%\third_party\_submodules\protobuf\cmake\build\protoc.exe" ( + set "PROTOC=%repoRoot%\third_party\_submodules\protobuf\cmake\build\protoc.exe" + ) else ( + where protoc >nul 2>nul + if !errorlevel! equ 0 ( + for /f "delims=" %%p in ('where protoc') do set "PROTOC=%%p" + ) else ( + echo Error: protoc not found. Please build protobuf submodule or install protoc. >&2 + exit /b 1 + ) + ) +) +REM Allow overriding protobuf include path via environment variable. +REM Default to local submodule source; fallback to system include path. +if not defined PROTOBUF_PROTO ( + if exist "%repoRoot%\third_party\_submodules\protobuf\src\google\protobuf" ( + set "PROTOBUF_PROTO=%repoRoot%\third_party\_submodules\protobuf\src" + ) else ( + for /f "delims=" %%p in ('where protoc 2^>nul') do set "_PROTOC_DIR=%%~dpp" + if defined _PROTOC_DIR ( + set "PROTOBUF_PROTO=!_PROTOC_DIR!..\include" + ) else ( + set "PROTOBUF_PROTO=%repoRoot%\third_party\_submodules\protobuf\src" + ) + ) +) +set "TABLEAU_PROTO=%repoRoot%\third_party\_submodules\tableau\proto" +set "ROOTDIR=%repoRoot%\test\csharp-tableau-loader" +set "PLGUIN_DIR=%repoRoot%\cmd\protoc-gen-csharp-tableau-loader" +set "PROTOCONF_IN=%repoRoot%\test\proto" +set "PROTOCONF_OUT=%ROOTDIR%\protoconf" +set "LOADER_OUT=%ROOTDIR%\tableau" + +REM remove old generated files +rmdir /s /q "%PROTOCONF_OUT%" "%LOADER_OUT%" 2>nul +mkdir "%PROTOCONF_OUT%" "%LOADER_OUT%" + +REM build protoc plugin of loader +pushd "%PLGUIN_DIR%" +go build +popd + +set "PATH=%PATH%;%PLGUIN_DIR%" + +set protoFiles= +pushd "%PROTOCONF_IN%" +for /R %%f in (*.proto) do ( + set protoFiles=!protoFiles! "%%f" +) +popd +"%PROTOC%" ^ +--csharp_out="%PROTOCONF_OUT%" ^ +--csharp-tableau-loader_out="%LOADER_OUT%" ^ +--csharp-tableau-loader_opt=paths=source_relative ^ +--proto_path="%PROTOBUF_PROTO%" ^ +--proto_path="%TABLEAU_PROTO%" ^ +--proto_path="%PROTOCONF_IN%" ^ +!protoFiles! + +set "TABLEAU_IN=%TABLEAU_PROTO%\tableau\protobuf" +set "TABLEAU_OUT=%ROOTDIR%\protoconf\tableau" +REM remove old generated files +rmdir /s /q "%TABLEAU_OUT%" 2>nul +mkdir "%TABLEAU_OUT%" + +"%PROTOC%" ^ +--csharp_out="%TABLEAU_OUT%" ^ +--proto_path="%PROTOBUF_PROTO%" ^ +--proto_path="%TABLEAU_PROTO%" ^ +"%TABLEAU_IN%\tableau.proto" "%TABLEAU_IN%\wellknown.proto" + +endlocal +endlocal diff --git a/test/csharp-tableau-loader/gen.sh b/test/csharp-tableau-loader/gen.sh new file mode 100755 index 00000000..7e3d06a9 --- /dev/null +++ b/test/csharp-tableau-loader/gen.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +# set -eux +set -e +set -o pipefail + +cd "$(git rev-parse --show-toplevel)" + +# Allow overriding protoc via environment variable. +# Default to locally compiled protoc for local development; fallback to system protoc. +LOCAL_PROTOC="./third_party/_submodules/protobuf/cmake/build/protoc" +if [ -z "$PROTOC" ]; then + if [ -x "$LOCAL_PROTOC" ]; then + PROTOC="$LOCAL_PROTOC" + else + PROTOC="$(which protoc 2>/dev/null || true)" + fi +fi +if [ -z "$PROTOC" ]; then + echo "Error: protoc not found. Please build protobuf submodule or install protoc." >&2 + exit 1 +fi +# Allow overriding protobuf include path via environment variable. +# Default to local submodule source; fallback to system include path. +LOCAL_PROTOBUF_PROTO="./third_party/_submodules/protobuf/src" +if [ -z "$PROTOBUF_PROTO" ]; then + if [ -d "$LOCAL_PROTOBUF_PROTO/google/protobuf" ]; then + PROTOBUF_PROTO="$LOCAL_PROTOBUF_PROTO" + else + PROTOBUF_PROTO="$(pkg-config --variable=includedir protobuf 2>/dev/null || echo /usr/include)" + fi +fi +TABLEAU_PROTO="./third_party/_submodules/tableau/proto" +ROOTDIR="./test/csharp-tableau-loader" +PLUGIN_DIR="./cmd/protoc-gen-csharp-tableau-loader" +PROTOCONF_IN="./test/proto" +PROTOCONF_OUT="${ROOTDIR}/protoconf" +LOADER_OUT="${ROOTDIR}/tableau" + +# remove old generated files +rm -rfv "$PROTOCONF_OUT" "$LOADER_OUT" +mkdir -p "$PROTOCONF_OUT" "$LOADER_OUT" + +# build +cd "${PLUGIN_DIR}" && go build && cd - + +export PATH="$(pwd)/${PLUGIN_DIR}:${PATH}" + +# Collect all .proto files (use `find` for cross-platform compatibility). +PROTO_FILES=$(find "$PROTOCONF_IN" -name "*.proto") + +${PROTOC} \ + --csharp_out="$PROTOCONF_OUT" \ + --csharp-tableau-loader_out="$LOADER_OUT" \ + --csharp-tableau-loader_opt=paths=source_relative \ + --proto_path="$PROTOBUF_PROTO" \ + --proto_path="$TABLEAU_PROTO" \ + --proto_path="$PROTOCONF_IN" \ + $PROTO_FILES + +TABLEAU_IN="$TABLEAU_PROTO/tableau/protobuf" +TABLEAU_OUT="${ROOTDIR}/protoconf/tableau" +# remove old generated files +rm -rfv "$TABLEAU_OUT" +mkdir -p "$TABLEAU_OUT" + +${PROTOC} \ + --csharp_out="$TABLEAU_OUT" \ + --proto_path="$PROTOBUF_PROTO" \ + --proto_path="$TABLEAU_PROTO" \ + "$TABLEAU_IN/tableau.proto" \ + "$TABLEAU_IN/wellknown.proto" diff --git a/test/csharp-tableau-loader/tableau/HeroConf.pc.cs b/test/csharp-tableau-loader/tableau/HeroConf.pc.cs new file mode 100644 index 00000000..576ea607 --- /dev/null +++ b/test/csharp-tableau-loader/tableau/HeroConf.pc.cs @@ -0,0 +1,181 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// source: hero_conf.proto +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// HeroConf is a wrapper around protobuf message Protoconf.HeroConf. + /// + public class HeroConf : Messager, IMessagerName + { + // OrderedMap types. + public class OrderedMap_Hero_AttrMap : SortedDictionary { } + + public class OrderedMap_HeroValue : Tuple + { + public OrderedMap_HeroValue(OrderedMap_Hero_AttrMap item1, Protoconf.HeroConf.Types.Hero item2) : base(item1, item2) { } + } + public class OrderedMap_HeroMap : SortedDictionary { } + + private OrderedMap_HeroMap _orderedMap = new OrderedMap_HeroMap(); + + private Protoconf.HeroConf _data = new(); + + /// + /// Name returns the HeroConf's message name. + /// + public string Name() => Protoconf.HeroConf.Descriptor.Name; + + /// + /// Load loads HeroConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.HeroConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.HeroConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load HeroConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the HeroConf's inner message data. + /// + public ref readonly Protoconf.HeroConf Data() => ref _data; + + /// + /// Message returns the HeroConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // OrderedMap init. + _orderedMap.Clear(); + foreach (var (key1, value1) in _data.HeroMap) + { + var ordered_map1 = new OrderedMap_Hero_AttrMap(); + foreach (var (key2, value2) in value1.AttrMap) + { + ordered_map1[key2] = value2; + } + _orderedMap[key1] = new OrderedMap_HeroValue(ordered_map1, value1); + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.HeroConf.Types.Hero? Get1(string name) => + _data.HeroMap?.TryGetValue(name, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.HeroConf.Types.Hero.Types.Attr? Get2(string name, string title) => + Get1(name)?.AttrMap?.TryGetValue(title, out var val) == true ? val : null; + + // OrderedMap accessers. + /// + /// GetOrderedMap returns the 1st-level ordered map. + /// + public ref readonly OrderedMap_HeroMap GetOrderedMap() => ref _orderedMap; + + /// + /// GetOrderedMap1 finds value in the 1st-level ordered map. + /// It will return null if the key is not found. + /// + public OrderedMap_Hero_AttrMap? GetOrderedMap1(string name) => + _orderedMap.TryGetValue(name, out var value) ? value.Item1 : null; + } + + /// + /// HeroBaseConf is a wrapper around protobuf message Protoconf.HeroBaseConf. + /// + public class HeroBaseConf : Messager, IMessagerName + { + private Protoconf.HeroBaseConf _data = new(); + + /// + /// Name returns the HeroBaseConf's message name. + /// + public string Name() => Protoconf.HeroBaseConf.Descriptor.Name; + + /// + /// Load loads HeroBaseConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.HeroBaseConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.HeroBaseConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load HeroBaseConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the HeroBaseConf's inner message data. + /// + public ref readonly Protoconf.HeroBaseConf Data() => ref _data; + + /// + /// Message returns the HeroBaseConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Base.Hero? Get1(string name) => + _data.HeroMap?.TryGetValue(name, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Base.Item? Get2(string name, string id) => + Get1(name)?.ItemMap?.TryGetValue(id, out var val) == true ? val : null; + } +} diff --git a/test/csharp-tableau-loader/tableau/Hub.pc.cs b/test/csharp-tableau-loader/tableau/Hub.pc.cs new file mode 100644 index 00000000..d957fe3f --- /dev/null +++ b/test/csharp-tableau-loader/tableau/Hub.pc.cs @@ -0,0 +1,242 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Threading; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// MessagerContainer holds all messager instances and provides fast access. + /// + internal class MessagerContainer + { + public Dictionary MessagerMap; + public DateTime LastLoadedTime; + public ActivityConf? ActivityConf; + public ChapterConf? ChapterConf; + public ThemeConf? ThemeConf; + public TaskConf? TaskConf; + public FruitConf? FruitConf; + public Fruit2Conf? Fruit2Conf; + public Fruit3Conf? Fruit3Conf; + public Fruit4Conf? Fruit4Conf; + public Fruit5Conf? Fruit5Conf; + public ItemConf? ItemConf; + public PatchReplaceConf? PatchReplaceConf; + public PatchMergeConf? PatchMergeConf; + public RecursivePatchConf? RecursivePatchConf; + public HeroConf? HeroConf; + public HeroBaseConf? HeroBaseConf; + + public MessagerContainer(Dictionary? messagerMap = null) + { + MessagerMap = messagerMap ?? new Dictionary(); + LastLoadedTime = DateTime.Now; + ActivityConf = InternalGet(messagerMap); + ChapterConf = InternalGet(messagerMap); + ThemeConf = InternalGet(messagerMap); + TaskConf = InternalGet(messagerMap); + FruitConf = InternalGet(messagerMap); + Fruit2Conf = InternalGet(messagerMap); + Fruit3Conf = InternalGet(messagerMap); + Fruit4Conf = InternalGet(messagerMap); + Fruit5Conf = InternalGet(messagerMap); + ItemConf = InternalGet(messagerMap); + PatchReplaceConf = InternalGet(messagerMap); + PatchMergeConf = InternalGet(messagerMap); + RecursivePatchConf = InternalGet(messagerMap); + HeroConf = InternalGet(messagerMap); + HeroBaseConf = InternalGet(messagerMap); + } + + /// + /// Get returns the messager of type T from the container. + /// + public T? Get() where T : Messager, IMessagerName, new() => InternalGet(MessagerMap); + + private static T? InternalGet(Dictionary? messagerMap) where T : Messager, IMessagerName, new() + { + if (messagerMap == null) return null; + string name = new T().Name(); + return messagerMap.TryGetValue(name, out var messager) ? (T)messager : null; + } + } + + /// + /// Atomic provides a thread-safe wrapper for reference types. + /// + internal class Atomic where T : class + { + private T? _value; + + public T? Value + { + get => Interlocked.CompareExchange(ref _value, null, null); + set => Interlocked.Exchange(ref _value, value); + } + } + + /// + /// HubOptions is the options for Hub. + /// + public class HubOptions + { + /// + /// Filter can only filter in certain specific messagers based on the + /// condition that you provide. + /// + public Func? Filter { get; set; } + } + + /// + /// Hub is the messager manager. It manages loading, accessing, and storing + /// all configuration messagers. + /// + public class Hub + { + private readonly Atomic _messagerContainer = new Atomic(); + private readonly HubOptions? _options; + + public Hub(HubOptions? options = null) + { + _options = options; + } + + /// + /// Load fills messages from files in the specified directory and format. + /// + public bool Load(string dir, Format fmt, in Load.Options? options = null) + { + var messagerMap = NewMessagerMap(); + var opts = options ?? new Load.Options(); + foreach (var kvs in messagerMap) + { + string name = kvs.Key; + if (!kvs.Value.Load(dir, fmt, opts.ParseMessagerOptionsByName(name))) + { + Console.Error.WriteLine($"load {name} failed: {Util.GetErrMsg()}"); + return false; + } + } + var tmpHub = new Hub(); + tmpHub.SetMessagerMap(messagerMap); + foreach (var messager in messagerMap) + { + if (!messager.Value.ProcessAfterLoadAll(tmpHub)) + { + Console.Error.WriteLine($"hub call ProcessAfterLoadAll failed, messager: {messager.Key}"); + return false; + } + } + SetMessagerMap(messagerMap); + return true; + } + + /// + /// GetMessagerMap returns the current messager map. + /// + public IReadOnlyDictionary? GetMessagerMap() => _messagerContainer.Value?.MessagerMap; + + /// + /// SetMessagerMap sets the messager map with thread-safe guarantee. + /// + public void SetMessagerMap(in Dictionary map) => _messagerContainer.Value = new MessagerContainer(map); + + /// + /// Get returns the messager of type T from the hub. + /// + public T? Get() where T : Messager, IMessagerName, new() => _messagerContainer.Value?.Get(); + + public ActivityConf? GetActivityConf() => _messagerContainer.Value?.ActivityConf; + + public ChapterConf? GetChapterConf() => _messagerContainer.Value?.ChapterConf; + + public ThemeConf? GetThemeConf() => _messagerContainer.Value?.ThemeConf; + + public TaskConf? GetTaskConf() => _messagerContainer.Value?.TaskConf; + + public FruitConf? GetFruitConf() => _messagerContainer.Value?.FruitConf; + + public Fruit2Conf? GetFruit2Conf() => _messagerContainer.Value?.Fruit2Conf; + + public Fruit3Conf? GetFruit3Conf() => _messagerContainer.Value?.Fruit3Conf; + + public Fruit4Conf? GetFruit4Conf() => _messagerContainer.Value?.Fruit4Conf; + + public Fruit5Conf? GetFruit5Conf() => _messagerContainer.Value?.Fruit5Conf; + + public ItemConf? GetItemConf() => _messagerContainer.Value?.ItemConf; + + public PatchReplaceConf? GetPatchReplaceConf() => _messagerContainer.Value?.PatchReplaceConf; + + public PatchMergeConf? GetPatchMergeConf() => _messagerContainer.Value?.PatchMergeConf; + + public RecursivePatchConf? GetRecursivePatchConf() => _messagerContainer.Value?.RecursivePatchConf; + + public HeroConf? GetHeroConf() => _messagerContainer.Value?.HeroConf; + + public HeroBaseConf? GetHeroBaseConf() => _messagerContainer.Value?.HeroBaseConf; + + /// + /// GetLastLoadedTime returns the time when hub's messager container was last set. + /// + public DateTime? GetLastLoadedTime() => _messagerContainer.Value?.LastLoadedTime; + + /// + /// NewMessagerMap creates a new MessagerMap based on the registered messagers. + /// + private Dictionary NewMessagerMap() + { + var messagerMap = new Dictionary(); + foreach (var kv in Registry.Registrar) + { + if (_options?.Filter?.Invoke(kv.Key) ?? true) + { + messagerMap[kv.Key] = kv.Value(); + } + } + return messagerMap; + } + } + + /// + /// Registry manages the registration of all messager generators. + /// + public class Registry + { + internal static readonly Dictionary> Registrar = new Dictionary>(); + + /// + /// Register registers a messager generator for type T. + /// + public static void Register() where T : Messager, IMessagerName, new() => Registrar[new T().Name()] = () => new T(); + + /// + /// Init registers all generated messagers. + /// + public static void Init() + { + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + Register(); + } + } +} diff --git a/test/csharp-tableau-loader/tableau/IndexConf.pc.cs b/test/csharp-tableau-loader/tableau/IndexConf.pc.cs new file mode 100644 index 00000000..e647e830 --- /dev/null +++ b/test/csharp-tableau-loader/tableau/IndexConf.pc.cs @@ -0,0 +1,1218 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// source: index_conf.proto +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// FruitConf is a wrapper around protobuf message Protoconf.FruitConf. + /// + public class FruitConf : Messager, IMessagerName + { + // OrderedIndex types. + // OrderedIndex: Price + public class OrderedIndex_ItemMap : SortedDictionary> { } + + private OrderedIndex_ItemMap _orderedIndexItemMap = new OrderedIndex_ItemMap(); + + private Dictionary _orderedIndexItemMap1 = new Dictionary(); + + private Protoconf.FruitConf _data = new(); + + /// + /// Name returns the FruitConf's message name. + /// + public string Name() => Protoconf.FruitConf.Descriptor.Name; + + /// + /// Load loads FruitConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.FruitConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.FruitConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load FruitConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the FruitConf's inner message data. + /// + public ref readonly Protoconf.FruitConf Data() => ref _data; + + /// + /// Message returns the FruitConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // OrderedIndex init. + _orderedIndexItemMap.Clear(); + _orderedIndexItemMap1.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.ItemMap) + { + { + // OrderedIndex: Price + var key = item2.Value.Price; + { + var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexItemMap[key] = new List(); + list.Add(item2.Value); + } + { + var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _orderedIndexItemMap1[k1] = new OrderedIndex_ItemMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item2.Value); + } + } + } + } + // OrderedIndex(sort): Price + Comparison orderedIndexItemMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexItemMap.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + foreach (var itemDict in _orderedIndexItemMap1.Values) + { + foreach (var itemList in itemDict.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.FruitConf.Types.Fruit? Get1(int fruitType) => + _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.FruitConf.Types.Fruit.Types.Item? Get2(int fruitType, int id) => + Get1(fruitType)?.ItemMap?.TryGetValue(id, out var val) == true ? val : null; + + // OrderedIndex: Price + + /// + /// FindItemMap finds the ordered index: key(Price) to value(Protoconf.FruitConf.Types.Fruit.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap; + + /// + /// FindItem finds a list of all values of the given key(s). + /// + public List? FindItem(int price) => + _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null; + + /// + /// FindFirstItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.FruitConf.Types.Fruit.Types.Item? FindFirstItem(int price) => + FindItem(price)?.FirstOrDefault(); + + /// + /// FindItemMap1 finds the ordered index: key(Price) to value(Protoconf.FruitConf.Types.Fruit.Types.Item), + /// which is the upper 1st-level sorted map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public OrderedIndex_ItemMap? FindItemMap1(int fruitType) => + _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType). + /// + public List? FindItem1(int fruitType, int price) => + FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null; + + /// + /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.FruitConf.Types.Fruit.Types.Item? FindFirstItem1(int fruitType, int price) => + FindItem1(fruitType, price)?.FirstOrDefault(); + } + + /// + /// Fruit2Conf is a wrapper around protobuf message Protoconf.Fruit2Conf. + /// + public class Fruit2Conf : Messager, IMessagerName + { + // Index types. + // Index: CountryName + public class Index_CountryMap : Dictionary> { } + + private Index_CountryMap _indexCountryMap = new Index_CountryMap(); + + // Index: CountryItemAttrName + public class Index_AttrMap : Dictionary> { } + + private Index_AttrMap _indexAttrMap = new Index_AttrMap(); + + private Dictionary _indexAttrMap1 = new Dictionary(); + + // OrderedIndex types. + // OrderedIndex: CountryItemPrice + public class OrderedIndex_ItemMap : SortedDictionary> { } + + private OrderedIndex_ItemMap _orderedIndexItemMap = new OrderedIndex_ItemMap(); + + private Dictionary _orderedIndexItemMap1 = new Dictionary(); + + private Protoconf.Fruit2Conf _data = new(); + + /// + /// Name returns the Fruit2Conf's message name. + /// + public string Name() => Protoconf.Fruit2Conf.Descriptor.Name; + + /// + /// Load loads Fruit2Conf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.Fruit2Conf)( + Tableau.Load.LoadMessagerInDir(Protoconf.Fruit2Conf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load Fruit2Conf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the Fruit2Conf's inner message data. + /// + public ref readonly Protoconf.Fruit2Conf Data() => ref _data; + + /// + /// Message returns the Fruit2Conf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // Index init. + _indexCountryMap.Clear(); + _indexAttrMap.Clear(); + _indexAttrMap1.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.CountryList) + { + { + // Index: CountryName + var key = item2.Name; + { + var list = _indexCountryMap.TryGetValue(key, out var existingList) ? + existingList : _indexCountryMap[key] = new List(); + list.Add(item2); + } + } + foreach (var item3 in item2.ItemMap) + { + foreach (var item4 in item3.Value.AttrList) + { + { + // Index: CountryItemAttrName + var key = item4.Name; + { + var list = _indexAttrMap.TryGetValue(key, out var existingList) ? + existingList : _indexAttrMap[key] = new List(); + list.Add(item4); + } + { + var map = _indexAttrMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexAttrMap1[k1] = new Index_AttrMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item4); + } + } + } + } + } + } + // OrderedIndex init. + _orderedIndexItemMap.Clear(); + _orderedIndexItemMap1.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.CountryList) + { + foreach (var item3 in item2.ItemMap) + { + { + // OrderedIndex: CountryItemPrice + var key = item3.Value.Price; + { + var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexItemMap[key] = new List(); + list.Add(item3.Value); + } + { + var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _orderedIndexItemMap1[k1] = new OrderedIndex_ItemMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item3.Value); + } + } + } + } + } + // OrderedIndex(sort): CountryItemPrice + Comparison orderedIndexItemMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexItemMap.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + foreach (var itemDict in _orderedIndexItemMap1.Values) + { + foreach (var itemList in itemDict.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit2Conf.Types.Fruit? Get1(int fruitType) => + _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null; + + // Index: CountryName + + /// + /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap; + + /// + /// FindCountry finds a list of all values of the given key(s). + /// + public List? FindCountry(string name) => + _indexCountryMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstCountry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit2Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) => + FindCountry(name)?.FirstOrDefault(); + + // Index: CountryItemAttrName + + /// + /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap; + + /// + /// FindAttr finds a list of all values of the given key(s). + /// + public List? FindAttr(string name) => + _indexAttrMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstAttr finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) => + FindAttr(name)?.FirstOrDefault(); + + /// + /// FindAttrMap1 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr), + /// which is the upper 1st-level map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_AttrMap? FindAttrMap1(int fruitType) => + _indexAttrMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindAttr1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (fruitType). + /// + public List? FindAttr1(int fruitType, string name) => + FindAttrMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null; + + /// + /// FindFirstAttr1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr1(int fruitType, string name) => + FindAttr1(fruitType, name)?.FirstOrDefault(); + + // OrderedIndex: CountryItemPrice + + /// + /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap; + + /// + /// FindItem finds a list of all values of the given key(s). + /// + public List? FindItem(int price) => + _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null; + + /// + /// FindFirstItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) => + FindItem(price)?.FirstOrDefault(); + + /// + /// FindItemMap1 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item), + /// which is the upper 1st-level sorted map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public OrderedIndex_ItemMap? FindItemMap1(int fruitType) => + _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType). + /// + public List? FindItem1(int fruitType, int price) => + FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null; + + /// + /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit2Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem1(int fruitType, int price) => + FindItem1(fruitType, price)?.FirstOrDefault(); + } + + /// + /// Fruit3Conf is a wrapper around protobuf message Protoconf.Fruit3Conf. + /// + public class Fruit3Conf : Messager, IMessagerName + { + // Index types. + // Index: CountryName + public class Index_CountryMap : Dictionary> { } + + private Index_CountryMap _indexCountryMap = new Index_CountryMap(); + + // Index: CountryItemAttrName + public class Index_AttrMap : Dictionary> { } + + private Index_AttrMap _indexAttrMap = new Index_AttrMap(); + + // OrderedIndex types. + // OrderedIndex: CountryItemPrice + public class OrderedIndex_ItemMap : SortedDictionary> { } + + private OrderedIndex_ItemMap _orderedIndexItemMap = new OrderedIndex_ItemMap(); + + private Protoconf.Fruit3Conf _data = new(); + + /// + /// Name returns the Fruit3Conf's message name. + /// + public string Name() => Protoconf.Fruit3Conf.Descriptor.Name; + + /// + /// Load loads Fruit3Conf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.Fruit3Conf)( + Tableau.Load.LoadMessagerInDir(Protoconf.Fruit3Conf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load Fruit3Conf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the Fruit3Conf's inner message data. + /// + public ref readonly Protoconf.Fruit3Conf Data() => ref _data; + + /// + /// Message returns the Fruit3Conf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // Index init. + _indexCountryMap.Clear(); + _indexAttrMap.Clear(); + foreach (var item1 in _data.FruitList) + { + foreach (var item2 in item1.CountryList) + { + { + // Index: CountryName + var key = item2.Name; + { + var list = _indexCountryMap.TryGetValue(key, out var existingList) ? + existingList : _indexCountryMap[key] = new List(); + list.Add(item2); + } + } + foreach (var item3 in item2.ItemMap) + { + foreach (var item4 in item3.Value.AttrList) + { + { + // Index: CountryItemAttrName + var key = item4.Name; + { + var list = _indexAttrMap.TryGetValue(key, out var existingList) ? + existingList : _indexAttrMap[key] = new List(); + list.Add(item4); + } + } + } + } + } + } + // OrderedIndex init. + _orderedIndexItemMap.Clear(); + foreach (var item1 in _data.FruitList) + { + foreach (var item2 in item1.CountryList) + { + foreach (var item3 in item2.ItemMap) + { + { + // OrderedIndex: CountryItemPrice + var key = item3.Value.Price; + { + var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexItemMap[key] = new List(); + list.Add(item3.Value); + } + } + } + } + } + // OrderedIndex(sort): CountryItemPrice + Comparison orderedIndexItemMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexItemMap.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + return true; + } + + // Index: CountryName + + /// + /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap; + + /// + /// FindCountry finds a list of all values of the given key(s). + /// + public List? FindCountry(string name) => + _indexCountryMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstCountry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit3Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) => + FindCountry(name)?.FirstOrDefault(); + + // Index: CountryItemAttrName + + /// + /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap; + + /// + /// FindAttr finds a list of all values of the given key(s). + /// + public List? FindAttr(string name) => + _indexAttrMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstAttr finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) => + FindAttr(name)?.FirstOrDefault(); + + // OrderedIndex: CountryItemPrice + + /// + /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap; + + /// + /// FindItem finds a list of all values of the given key(s). + /// + public List? FindItem(int price) => + _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null; + + /// + /// FindFirstItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit3Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) => + FindItem(price)?.FirstOrDefault(); + } + + /// + /// Fruit4Conf is a wrapper around protobuf message Protoconf.Fruit4Conf. + /// + public class Fruit4Conf : Messager, IMessagerName + { + + // LevelIndex keys. + public readonly struct LevelIndex_Fruit_CountryKey : IEquatable + { + public int FruitType { get; } + public int Id { get; } + + public LevelIndex_Fruit_CountryKey(int fruitType, int id) + { + FruitType = fruitType; + Id = id; + } + + public bool Equals(LevelIndex_Fruit_CountryKey other) => + (FruitType, Id).Equals((other.FruitType, other.Id)); + + public override int GetHashCode() => + (FruitType, Id).GetHashCode(); + } + + // Index types. + // Index: CountryName + public class Index_CountryMap : Dictionary> { } + + private Index_CountryMap _indexCountryMap = new Index_CountryMap(); + + private Dictionary _indexCountryMap1 = new Dictionary(); + + // Index: CountryItemAttrName + public class Index_AttrMap : Dictionary> { } + + private Index_AttrMap _indexAttrMap = new Index_AttrMap(); + + private Dictionary _indexAttrMap1 = new Dictionary(); + + private Dictionary _indexAttrMap2 = new Dictionary(); + + // OrderedIndex types. + // OrderedIndex: CountryItemPrice + public class OrderedIndex_ItemMap : SortedDictionary> { } + + private OrderedIndex_ItemMap _orderedIndexItemMap = new OrderedIndex_ItemMap(); + + private Dictionary _orderedIndexItemMap1 = new Dictionary(); + + private Dictionary _orderedIndexItemMap2 = new Dictionary(); + + private Protoconf.Fruit4Conf _data = new(); + + /// + /// Name returns the Fruit4Conf's message name. + /// + public string Name() => Protoconf.Fruit4Conf.Descriptor.Name; + + /// + /// Load loads Fruit4Conf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.Fruit4Conf)( + Tableau.Load.LoadMessagerInDir(Protoconf.Fruit4Conf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load Fruit4Conf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the Fruit4Conf's inner message data. + /// + public ref readonly Protoconf.Fruit4Conf Data() => ref _data; + + /// + /// Message returns the Fruit4Conf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // Index init. + _indexCountryMap.Clear(); + _indexCountryMap1.Clear(); + _indexAttrMap.Clear(); + _indexAttrMap1.Clear(); + _indexAttrMap2.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.CountryMap) + { + var k2 = item2.Key; + { + // Index: CountryName + var key = item2.Value.Name; + { + var list = _indexCountryMap.TryGetValue(key, out var existingList) ? + existingList : _indexCountryMap[key] = new List(); + list.Add(item2.Value); + } + { + var map = _indexCountryMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexCountryMap1[k1] = new Index_CountryMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item2.Value); + } + } + foreach (var item3 in item2.Value.ItemMap) + { + foreach (var item4 in item3.Value.AttrList) + { + { + // Index: CountryItemAttrName + var key = item4.Name; + { + var list = _indexAttrMap.TryGetValue(key, out var existingList) ? + existingList : _indexAttrMap[key] = new List(); + list.Add(item4); + } + { + var map = _indexAttrMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexAttrMap1[k1] = new Index_AttrMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item4); + } + { + var mapKey = new LevelIndex_Fruit_CountryKey(k1, k2); + var map = _indexAttrMap2.TryGetValue(mapKey, out var existingMap) ? + existingMap : _indexAttrMap2[mapKey] = new Index_AttrMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item4); + } + } + } + } + } + } + // OrderedIndex init. + _orderedIndexItemMap.Clear(); + _orderedIndexItemMap1.Clear(); + _orderedIndexItemMap2.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.CountryMap) + { + var k2 = item2.Key; + foreach (var item3 in item2.Value.ItemMap) + { + { + // OrderedIndex: CountryItemPrice + var key = item3.Value.Price; + { + var list = _orderedIndexItemMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexItemMap[key] = new List(); + list.Add(item3.Value); + } + { + var map = _orderedIndexItemMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _orderedIndexItemMap1[k1] = new OrderedIndex_ItemMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item3.Value); + } + { + var mapKey = new LevelIndex_Fruit_CountryKey(k1, k2); + var map = _orderedIndexItemMap2.TryGetValue(mapKey, out var existingMap) ? + existingMap : _orderedIndexItemMap2[mapKey] = new OrderedIndex_ItemMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item3.Value); + } + } + } + } + } + // OrderedIndex(sort): CountryItemPrice + Comparison orderedIndexItemMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexItemMap.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + foreach (var itemDict in _orderedIndexItemMap1.Values) + { + foreach (var itemList in itemDict.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + } + foreach (var itemDict in _orderedIndexItemMap2.Values) + { + foreach (var itemList in itemDict.Values) + { + itemList.Sort(orderedIndexItemMapComparison); + } + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit4Conf.Types.Fruit? Get1(int fruitType) => + _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? Get2(int fruitType, int id) => + Get1(fruitType)?.CountryMap?.TryGetValue(id, out var val) == true ? val : null; + + /// + /// Get3 finds value in the 3rd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? Get3(int fruitType, int id, int id3) => + Get2(fruitType, id)?.ItemMap?.TryGetValue(id3, out var val) == true ? val : null; + + // Index: CountryName + + /// + /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap; + + /// + /// FindCountry finds a list of all values of the given key(s). + /// + public List? FindCountry(string name) => + _indexCountryMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstCountry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) => + FindCountry(name)?.FirstOrDefault(); + + /// + /// FindCountryMap1 finds the index: key(CountryName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country), + /// which is the upper 1st-level map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_CountryMap? FindCountryMap1(int fruitType) => + _indexCountryMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindCountry1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (fruitType). + /// + public List? FindCountry1(int fruitType, string name) => + FindCountryMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null; + + /// + /// FindFirstCountry1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country? FindFirstCountry1(int fruitType, string name) => + FindCountry1(fruitType, name)?.FirstOrDefault(); + + // Index: CountryItemAttrName + + /// + /// FindAttrMap finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_AttrMap FindAttrMap() => ref _indexAttrMap; + + /// + /// FindAttr finds a list of all values of the given key(s). + /// + public List? FindAttr(string name) => + _indexAttrMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstAttr finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr(string name) => + FindAttr(name)?.FirstOrDefault(); + + /// + /// FindAttrMap1 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr), + /// which is the upper 1st-level map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_AttrMap? FindAttrMap1(int fruitType) => + _indexAttrMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindAttr1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (fruitType). + /// + public List? FindAttr1(int fruitType, string name) => + FindAttrMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null; + + /// + /// FindFirstAttr1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr1(int fruitType, string name) => + FindAttr1(fruitType, name)?.FirstOrDefault(); + + /// + /// FindAttrMap2 finds the index: key(CountryItemAttrName) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr), + /// which is the upper 2nd-level map specified by (fruitType, id). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_AttrMap? FindAttrMap2(int fruitType, int id) => + _indexAttrMap2.TryGetValue(new LevelIndex_Fruit_CountryKey(fruitType, id), out var value) ? value : null; + + /// + /// FindAttr2 finds a list of all values of the given key(s) in the upper 2nd-level map + /// specified by (fruitType, id). + /// + public List? FindAttr2(int fruitType, int id, string name) => + FindAttrMap2(fruitType, id)?.TryGetValue(name, out var value) == true ? value : null; + + /// + /// FindFirstAttr2 finds the first value of the given key(s) in the upper 2nd-level map + /// specified by (fruitType, id), or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item.Types.Attr? FindFirstAttr2(int fruitType, int id, string name) => + FindAttr2(fruitType, id, name)?.FirstOrDefault(); + + // OrderedIndex: CountryItemPrice + + /// + /// FindItemMap finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ItemMap FindItemMap() => ref _orderedIndexItemMap; + + /// + /// FindItem finds a list of all values of the given key(s). + /// + public List? FindItem(int price) => + _orderedIndexItemMap.TryGetValue(price, out var value) ? value : null; + + /// + /// FindFirstItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem(int price) => + FindItem(price)?.FirstOrDefault(); + + /// + /// FindItemMap1 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item), + /// which is the upper 1st-level sorted map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public OrderedIndex_ItemMap? FindItemMap1(int fruitType) => + _orderedIndexItemMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindItem1 finds a list of all values of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType). + /// + public List? FindItem1(int fruitType, int price) => + FindItemMap1(fruitType)?.TryGetValue(price, out var value) == true ? value : null; + + /// + /// FindFirstItem1 finds the first value of the given key(s) in the upper 1st-level sorted map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem1(int fruitType, int price) => + FindItem1(fruitType, price)?.FirstOrDefault(); + + /// + /// FindItemMap2 finds the ordered index: key(CountryItemPrice) to value(Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item), + /// which is the upper 2nd-level sorted map specified by (fruitType, id). + /// One key may correspond to multiple values, which are represented by a list. + /// + public OrderedIndex_ItemMap? FindItemMap2(int fruitType, int id) => + _orderedIndexItemMap2.TryGetValue(new LevelIndex_Fruit_CountryKey(fruitType, id), out var value) ? value : null; + + /// + /// FindItem2 finds a list of all values of the given key(s) in the upper 2nd-level sorted map + /// specified by (fruitType, id). + /// + public List? FindItem2(int fruitType, int id, int price) => + FindItemMap2(fruitType, id)?.TryGetValue(price, out var value) == true ? value : null; + + /// + /// FindFirstItem2 finds the first value of the given key(s) in the upper 2nd-level sorted map + /// specified by (fruitType, id), or null if no value found. + /// + public Protoconf.Fruit4Conf.Types.Fruit.Types.Country.Types.Item? FindFirstItem2(int fruitType, int id, int price) => + FindItem2(fruitType, id, price)?.FirstOrDefault(); + } + + /// + /// Fruit5Conf is a wrapper around protobuf message Protoconf.Fruit5Conf. + /// + public class Fruit5Conf : Messager, IMessagerName + { + // Index types. + // Index: CountryName + public class Index_CountryMap : Dictionary> { } + + private Index_CountryMap _indexCountryMap = new Index_CountryMap(); + + private Dictionary _indexCountryMap1 = new Dictionary(); + + private Protoconf.Fruit5Conf _data = new(); + + /// + /// Name returns the Fruit5Conf's message name. + /// + public string Name() => Protoconf.Fruit5Conf.Descriptor.Name; + + /// + /// Load loads Fruit5Conf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.Fruit5Conf)( + Tableau.Load.LoadMessagerInDir(Protoconf.Fruit5Conf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load Fruit5Conf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the Fruit5Conf's inner message data. + /// + public ref readonly Protoconf.Fruit5Conf Data() => ref _data; + + /// + /// Message returns the Fruit5Conf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // Index init. + _indexCountryMap.Clear(); + _indexCountryMap1.Clear(); + foreach (var item1 in _data.FruitMap) + { + var k1 = item1.Key; + foreach (var item2 in item1.Value.CountryMap) + { + { + // Index: CountryName + var key = item2.Value.Name; + { + var list = _indexCountryMap.TryGetValue(key, out var existingList) ? + existingList : _indexCountryMap[key] = new List(); + list.Add(item2.Value); + } + { + var map = _indexCountryMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexCountryMap1[k1] = new Index_CountryMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item2.Value); + } + } + } + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit5Conf.Types.Fruit? Get1(int fruitType) => + _data.FruitMap?.TryGetValue(fruitType, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? Get2(int fruitType, int id) => + Get1(fruitType)?.CountryMap?.TryGetValue(id, out var val) == true ? val : null; + + /// + /// Get3 finds value in the 3rd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Fruit5Conf.Types.Fruit.Types.Country.Types.Item? Get3(int fruitType, int id, int id3) => + Get2(fruitType, id)?.ItemMap?.TryGetValue(id3, out var val) == true ? val : null; + + // Index: CountryName + + /// + /// FindCountryMap finds the index: key(CountryName) to value(Protoconf.Fruit5Conf.Types.Fruit.Types.Country) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_CountryMap FindCountryMap() => ref _indexCountryMap; + + /// + /// FindCountry finds a list of all values of the given key(s). + /// + public List? FindCountry(string name) => + _indexCountryMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstCountry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? FindFirstCountry(string name) => + FindCountry(name)?.FirstOrDefault(); + + /// + /// FindCountryMap1 finds the index: key(CountryName) to value(Protoconf.Fruit5Conf.Types.Fruit.Types.Country), + /// which is the upper 1st-level map specified by (fruitType). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_CountryMap? FindCountryMap1(int fruitType) => + _indexCountryMap1.TryGetValue(fruitType, out var value) ? value : null; + + /// + /// FindCountry1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (fruitType). + /// + public List? FindCountry1(int fruitType, string name) => + FindCountryMap1(fruitType)?.TryGetValue(name, out var value) == true ? value : null; + + /// + /// FindFirstCountry1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (fruitType), or null if no value found. + /// + public Protoconf.Fruit5Conf.Types.Fruit.Types.Country? FindFirstCountry1(int fruitType, string name) => + FindCountry1(fruitType, name)?.FirstOrDefault(); + } +} diff --git a/test/csharp-tableau-loader/tableau/ItemConf.pc.cs b/test/csharp-tableau-loader/tableau/ItemConf.pc.cs new file mode 100644 index 00000000..19f884ef --- /dev/null +++ b/test/csharp-tableau-loader/tableau/ItemConf.pc.cs @@ -0,0 +1,633 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// source: item_conf.proto +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// ItemConf is a wrapper around protobuf message Protoconf.ItemConf. + /// + public class ItemConf : Messager, IMessagerName + { + // OrderedMap types. + public class OrderedMap_ItemMap : SortedDictionary { } + + private OrderedMap_ItemMap _orderedMap = new OrderedMap_ItemMap(); + + // Index types. + // Index: Type + public class Index_ItemMap : Dictionary> { } + + private Index_ItemMap _indexItemMap = new Index_ItemMap(); + + // Index: Param@ItemInfo + public class Index_ItemInfoMap : Dictionary> { } + + private Index_ItemInfoMap _indexItemInfoMap = new Index_ItemInfoMap(); + + // Index: Default@ItemDefaultInfo + public class Index_ItemDefaultInfoMap : Dictionary> { } + + private Index_ItemDefaultInfoMap _indexItemDefaultInfoMap = new Index_ItemDefaultInfoMap(); + + // Index: ExtType@ItemExtInfo + public class Index_ItemExtInfoMap : Dictionary> { } + + private Index_ItemExtInfoMap _indexItemExtInfoMap = new Index_ItemExtInfoMap(); + + // Index: (ID,Name)@AwardItem + public readonly struct Index_AwardItemKey : IEquatable + { + public uint Id { get; } + public string Name { get; } + + public Index_AwardItemKey(uint id, string name) + { + Id = id; + Name = name; + } + + public bool Equals(Index_AwardItemKey other) => + (Id, Name).Equals((other.Id, other.Name)); + + public override int GetHashCode() => + (Id, Name).GetHashCode(); + } + + public class Index_AwardItemMap : Dictionary> { } + + private Index_AwardItemMap _indexAwardItemMap = new Index_AwardItemMap(); + + // Index: (ID,Type,Param,ExtType)@SpecialItem + public readonly struct Index_SpecialItemKey : IEquatable + { + public uint Id { get; } + public Protoconf.FruitType Type { get; } + public int Param { get; } + public Protoconf.FruitType ExtType { get; } + + public Index_SpecialItemKey(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType) + { + Id = id; + Type = type; + Param = param; + ExtType = extType; + } + + public bool Equals(Index_SpecialItemKey other) => + (Id, Type, Param, ExtType).Equals((other.Id, other.Type, other.Param, other.ExtType)); + + public override int GetHashCode() => + (Id, Type, Param, ExtType).GetHashCode(); + } + + public class Index_SpecialItemMap : Dictionary> { } + + private Index_SpecialItemMap _indexSpecialItemMap = new Index_SpecialItemMap(); + + // Index: PathDir@ItemPathDir + public class Index_ItemPathDirMap : Dictionary> { } + + private Index_ItemPathDirMap _indexItemPathDirMap = new Index_ItemPathDirMap(); + + // Index: PathName@ItemPathName + public class Index_ItemPathNameMap : Dictionary> { } + + private Index_ItemPathNameMap _indexItemPathNameMap = new Index_ItemPathNameMap(); + + // Index: PathFriendID@ItemPathFriendID + public class Index_ItemPathFriendIDMap : Dictionary> { } + + private Index_ItemPathFriendIDMap _indexItemPathFriendIdMap = new Index_ItemPathFriendIDMap(); + + // Index: UseEffectType@UseEffectType + public class Index_UseEffectTypeMap : Dictionary> { } + + private Index_UseEffectTypeMap _indexUseEffectTypeMap = new Index_UseEffectTypeMap(); + + // OrderedIndex types. + // OrderedIndex: ExtType@ExtType + public class OrderedIndex_ExtTypeMap : SortedDictionary> { } + + private OrderedIndex_ExtTypeMap _orderedIndexExtTypeMap = new OrderedIndex_ExtTypeMap(); + + // OrderedIndex: (Param,ExtType)@ParamExtType + public readonly struct OrderedIndex_ParamExtTypeKey : IComparable + { + public int Param { get; } + public Protoconf.FruitType ExtType { get; } + + public OrderedIndex_ParamExtTypeKey(int param, Protoconf.FruitType extType) + { + Param = param; + ExtType = extType; + } + + public int CompareTo(OrderedIndex_ParamExtTypeKey other) => + (Param, ExtType).CompareTo((other.Param, other.ExtType)); + } + + public class OrderedIndex_ParamExtTypeMap : SortedDictionary> { } + + private OrderedIndex_ParamExtTypeMap _orderedIndexParamExtTypeMap = new OrderedIndex_ParamExtTypeMap(); + + private Protoconf.ItemConf _data = new(); + + /// + /// Name returns the ItemConf's message name. + /// + public string Name() => Protoconf.ItemConf.Descriptor.Name; + + /// + /// Load loads ItemConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.ItemConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.ItemConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load ItemConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the ItemConf's inner message data. + /// + public ref readonly Protoconf.ItemConf Data() => ref _data; + + /// + /// Message returns the ItemConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // OrderedMap init. + _orderedMap.Clear(); + foreach (var (key1, value1) in _data.ItemMap) + { + _orderedMap[key1] = value1; + } + // Index init. + _indexItemMap.Clear(); + _indexItemInfoMap.Clear(); + _indexItemDefaultInfoMap.Clear(); + _indexItemExtInfoMap.Clear(); + _indexAwardItemMap.Clear(); + _indexSpecialItemMap.Clear(); + _indexItemPathDirMap.Clear(); + _indexItemPathNameMap.Clear(); + _indexItemPathFriendIdMap.Clear(); + _indexUseEffectTypeMap.Clear(); + foreach (var item1 in _data.ItemMap) + { + { + // Index: Type + var key = item1.Value.Type; + { + var list = _indexItemMap.TryGetValue(key, out var existingList) ? + existingList : _indexItemMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // Index: Param@ItemInfo + foreach (var item2 in item1.Value.ParamList ?? Enumerable.Empty()) + { + { + var list = _indexItemInfoMap.TryGetValue(item2, out var existingList) ? + existingList : _indexItemInfoMap[item2] = new List(); + list.Add(item1.Value); + } + } + } + { + // Index: Default@ItemDefaultInfo + var key = item1.Value.Default; + { + var list = _indexItemDefaultInfoMap.TryGetValue(key, out var existingList) ? + existingList : _indexItemDefaultInfoMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // Index: ExtType@ItemExtInfo + foreach (var item2 in item1.Value.ExtTypeList ?? Enumerable.Empty()) + { + { + var list = _indexItemExtInfoMap.TryGetValue(item2, out var existingList) ? + existingList : _indexItemExtInfoMap[item2] = new List(); + list.Add(item1.Value); + } + } + } + { + // Index: (ID,Name)@AwardItem + var key = new Index_AwardItemKey(item1.Value.Id, item1.Value.Name); + { + var list = _indexAwardItemMap.TryGetValue(key, out var existingList) ? + existingList : _indexAwardItemMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // Index: (ID,Type,Param,ExtType)@SpecialItem + foreach (var indexItem2 in item1.Value.ParamList ?? Enumerable.Empty()) + { + foreach (var indexItem3 in item1.Value.ExtTypeList ?? Enumerable.Empty()) + { + var key = new Index_SpecialItemKey(item1.Value.Id, item1.Value.Type, indexItem2, indexItem3); + { + var list = _indexSpecialItemMap.TryGetValue(key, out var existingList) ? + existingList : _indexSpecialItemMap[key] = new List(); + list.Add(item1.Value); + } + } + } + } + { + // Index: PathDir@ItemPathDir + var key = item1.Value.Path?.Dir ?? ""; + { + var list = _indexItemPathDirMap.TryGetValue(key, out var existingList) ? + existingList : _indexItemPathDirMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // Index: PathName@ItemPathName + foreach (var item2 in item1.Value.Path?.NameList ?? Enumerable.Empty()) + { + { + var list = _indexItemPathNameMap.TryGetValue(item2, out var existingList) ? + existingList : _indexItemPathNameMap[item2] = new List(); + list.Add(item1.Value); + } + } + } + { + // Index: PathFriendID@ItemPathFriendID + var key = item1.Value.Path?.Friend?.Id ?? 0; + { + var list = _indexItemPathFriendIdMap.TryGetValue(key, out var existingList) ? + existingList : _indexItemPathFriendIdMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // Index: UseEffectType@UseEffectType + var key = item1.Value.UseEffect?.Type ?? 0; + { + var list = _indexUseEffectTypeMap.TryGetValue(key, out var existingList) ? + existingList : _indexUseEffectTypeMap[key] = new List(); + list.Add(item1.Value); + } + } + } + // Index(sort): Param@ItemInfo + Comparison indexItemInfoMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _indexItemInfoMap.Values) + { + itemList.Sort(indexItemInfoMapComparison); + } + // Index(sort): (ID,Name)@AwardItem + Comparison indexAwardItemMapComparison = (a, b) => + (a.Type, a.UseEffect?.Type ?? 0).CompareTo((b.Type, b.UseEffect?.Type ?? 0)); + foreach (var itemList in _indexAwardItemMap.Values) + { + itemList.Sort(indexAwardItemMapComparison); + } + // OrderedIndex init. + _orderedIndexExtTypeMap.Clear(); + _orderedIndexParamExtTypeMap.Clear(); + foreach (var item1 in _data.ItemMap) + { + { + // OrderedIndex: ExtType@ExtType + foreach (var item2 in item1.Value.ExtTypeList ?? Enumerable.Empty()) + { + var key = item2; + { + var list = _orderedIndexExtTypeMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexExtTypeMap[key] = new List(); + list.Add(item1.Value); + } + } + } + { + // OrderedIndex: (Param,ExtType)@ParamExtType + foreach (var indexItem0 in item1.Value.ParamList ?? Enumerable.Empty()) + { + foreach (var indexItem1 in item1.Value.ExtTypeList ?? Enumerable.Empty()) + { + var key = new OrderedIndex_ParamExtTypeKey(indexItem0, indexItem1); + { + var list = _orderedIndexParamExtTypeMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexParamExtTypeMap[key] = new List(); + list.Add(item1.Value); + } + } + } + } + } + // OrderedIndex(sort): (Param,ExtType)@ParamExtType + Comparison orderedIndexParamExtTypeMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexParamExtTypeMap.Values) + { + itemList.Sort(orderedIndexParamExtTypeMapComparison); + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.ItemConf.Types.Item? Get1(uint id) => + _data.ItemMap?.TryGetValue(id, out var val) == true ? val : null; + + // OrderedMap accessers. + /// + /// GetOrderedMap returns the 1st-level ordered map. + /// + public ref readonly OrderedMap_ItemMap GetOrderedMap() => ref _orderedMap; + + // Index: Type + + /// + /// FindItemMap finds the index: key(Type) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemMap FindItemMap() => ref _indexItemMap; + + /// + /// FindItem finds a list of all values of the given key(s). + /// + public List? FindItem(Protoconf.FruitType type) => + _indexItemMap.TryGetValue(type, out var value) ? value : null; + + /// + /// FindFirstItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItem(Protoconf.FruitType type) => + FindItem(type)?.FirstOrDefault(); + + // Index: Param@ItemInfo + + /// + /// FindItemInfoMap finds the index: key(Param@ItemInfo) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemInfoMap FindItemInfoMap() => ref _indexItemInfoMap; + + /// + /// FindItemInfo finds a list of all values of the given key(s). + /// + public List? FindItemInfo(int param) => + _indexItemInfoMap.TryGetValue(param, out var value) ? value : null; + + /// + /// FindFirstItemInfo finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemInfo(int param) => + FindItemInfo(param)?.FirstOrDefault(); + + // Index: Default@ItemDefaultInfo + + /// + /// FindItemDefaultInfoMap finds the index: key(Default@ItemDefaultInfo) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemDefaultInfoMap FindItemDefaultInfoMap() => ref _indexItemDefaultInfoMap; + + /// + /// FindItemDefaultInfo finds a list of all values of the given key(s). + /// + public List? FindItemDefaultInfo(string default_) => + _indexItemDefaultInfoMap.TryGetValue(default_, out var value) ? value : null; + + /// + /// FindFirstItemDefaultInfo finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemDefaultInfo(string default_) => + FindItemDefaultInfo(default_)?.FirstOrDefault(); + + // Index: ExtType@ItemExtInfo + + /// + /// FindItemExtInfoMap finds the index: key(ExtType@ItemExtInfo) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemExtInfoMap FindItemExtInfoMap() => ref _indexItemExtInfoMap; + + /// + /// FindItemExtInfo finds a list of all values of the given key(s). + /// + public List? FindItemExtInfo(Protoconf.FruitType extType) => + _indexItemExtInfoMap.TryGetValue(extType, out var value) ? value : null; + + /// + /// FindFirstItemExtInfo finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemExtInfo(Protoconf.FruitType extType) => + FindItemExtInfo(extType)?.FirstOrDefault(); + + // Index: (ID,Name)@AwardItem + + /// + /// FindAwardItemMap finds the index: key((ID,Name)@AwardItem) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_AwardItemMap FindAwardItemMap() => ref _indexAwardItemMap; + + /// + /// FindAwardItem finds a list of all values of the given key(s). + /// + public List? FindAwardItem(uint id, string name) => + _indexAwardItemMap.TryGetValue(new Index_AwardItemKey(id, name), out var value) ? value : null; + + /// + /// FindFirstAwardItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstAwardItem(uint id, string name) => + FindAwardItem(id, name)?.FirstOrDefault(); + + // Index: (ID,Type,Param,ExtType)@SpecialItem + + /// + /// FindSpecialItemMap finds the index: key((ID,Type,Param,ExtType)@SpecialItem) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_SpecialItemMap FindSpecialItemMap() => ref _indexSpecialItemMap; + + /// + /// FindSpecialItem finds a list of all values of the given key(s). + /// + public List? FindSpecialItem(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType) => + _indexSpecialItemMap.TryGetValue(new Index_SpecialItemKey(id, type, param, extType), out var value) ? value : null; + + /// + /// FindFirstSpecialItem finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstSpecialItem(uint id, Protoconf.FruitType type, int param, Protoconf.FruitType extType) => + FindSpecialItem(id, type, param, extType)?.FirstOrDefault(); + + // Index: PathDir@ItemPathDir + + /// + /// FindItemPathDirMap finds the index: key(PathDir@ItemPathDir) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemPathDirMap FindItemPathDirMap() => ref _indexItemPathDirMap; + + /// + /// FindItemPathDir finds a list of all values of the given key(s). + /// + public List? FindItemPathDir(string dir) => + _indexItemPathDirMap.TryGetValue(dir, out var value) ? value : null; + + /// + /// FindFirstItemPathDir finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemPathDir(string dir) => + FindItemPathDir(dir)?.FirstOrDefault(); + + // Index: PathName@ItemPathName + + /// + /// FindItemPathNameMap finds the index: key(PathName@ItemPathName) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemPathNameMap FindItemPathNameMap() => ref _indexItemPathNameMap; + + /// + /// FindItemPathName finds a list of all values of the given key(s). + /// + public List? FindItemPathName(string name) => + _indexItemPathNameMap.TryGetValue(name, out var value) ? value : null; + + /// + /// FindFirstItemPathName finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemPathName(string name) => + FindItemPathName(name)?.FirstOrDefault(); + + // Index: PathFriendID@ItemPathFriendID + + /// + /// FindItemPathFriendIDMap finds the index: key(PathFriendID@ItemPathFriendID) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ItemPathFriendIDMap FindItemPathFriendIDMap() => ref _indexItemPathFriendIdMap; + + /// + /// FindItemPathFriendID finds a list of all values of the given key(s). + /// + public List? FindItemPathFriendID(uint id) => + _indexItemPathFriendIdMap.TryGetValue(id, out var value) ? value : null; + + /// + /// FindFirstItemPathFriendID finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstItemPathFriendID(uint id) => + FindItemPathFriendID(id)?.FirstOrDefault(); + + // Index: UseEffectType@UseEffectType + + /// + /// FindUseEffectTypeMap finds the index: key(UseEffectType@UseEffectType) to value(Protoconf.ItemConf.Types.Item) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_UseEffectTypeMap FindUseEffectTypeMap() => ref _indexUseEffectTypeMap; + + /// + /// FindUseEffectType finds a list of all values of the given key(s). + /// + public List? FindUseEffectType(Protoconf.UseEffect.Types.Type type) => + _indexUseEffectTypeMap.TryGetValue(type, out var value) ? value : null; + + /// + /// FindFirstUseEffectType finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstUseEffectType(Protoconf.UseEffect.Types.Type type) => + FindUseEffectType(type)?.FirstOrDefault(); + + // OrderedIndex: ExtType@ExtType + + /// + /// FindExtTypeMap finds the ordered index: key(ExtType@ExtType) to value(Protoconf.ItemConf.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ExtTypeMap FindExtTypeMap() => ref _orderedIndexExtTypeMap; + + /// + /// FindExtType finds a list of all values of the given key(s). + /// + public List? FindExtType(Protoconf.FruitType extType) => + _orderedIndexExtTypeMap.TryGetValue(extType, out var value) ? value : null; + + /// + /// FindFirstExtType finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstExtType(Protoconf.FruitType extType) => + FindExtType(extType)?.FirstOrDefault(); + + // OrderedIndex: (Param,ExtType)@ParamExtType + + /// + /// FindParamExtTypeMap finds the ordered index: key((Param,ExtType)@ParamExtType) to value(Protoconf.ItemConf.Types.Item) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ParamExtTypeMap FindParamExtTypeMap() => ref _orderedIndexParamExtTypeMap; + + /// + /// FindParamExtType finds a list of all values of the given key(s). + /// + public List? FindParamExtType(int param, Protoconf.FruitType extType) => + _orderedIndexParamExtTypeMap.TryGetValue(new OrderedIndex_ParamExtTypeKey(param, extType), out var value) ? value : null; + + /// + /// FindFirstParamExtType finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ItemConf.Types.Item? FindFirstParamExtType(int param, Protoconf.FruitType extType) => + FindParamExtType(param, extType)?.FirstOrDefault(); + } +} diff --git a/test/csharp-tableau-loader/tableau/Load.pc.cs b/test/csharp-tableau-loader/tableau/Load.pc.cs new file mode 100644 index 00000000..463f9aa8 --- /dev/null +++ b/test/csharp-tableau-loader/tableau/Load.pc.cs @@ -0,0 +1,231 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// +#nullable enable +using System; +using System.Collections.Generic; +using System.IO; +using pb = global::Google.Protobuf; +using pbr = global::Google.Protobuf.Reflection; +namespace Tableau +{ + /// + /// Load provides functions for loading and parsing protobuf messages from files. + /// + public static class Load + { + /// + /// ReadFunc reads the config file and returns its content. + /// + public delegate byte[] ReadFunc(string path); + /// + /// LoadFunc defines a func which can load message's content based on the + /// given descriptor, path, format, and options. + /// + public delegate pb::IMessage? LoadFunc(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options); + + /// + /// BaseOptions is the common options for both global-level and messager-level options. + /// + public class BaseOptions + { + /// + /// Whether to ignore unknown JSON fields during parsing. + /// + public bool? IgnoreUnknownFields { get; set; } + /// + /// You can specify custom read function to read a config file's content. + /// Default is File.ReadAllBytes. + /// + public ReadFunc? ReadFunc { get; set; } + /// + /// You can specify custom load function to load a messager's content. + /// Default is LoadMessager. + /// + public LoadFunc? LoadFunc { get; set; } + } + + /// + /// Options is the global-level options, which contains both global-level and + /// messager-level options. + /// + public class Options : BaseOptions + { + /// + /// MessagerOptions maps each messager name to a MessagerOptions. + /// If specified, then the messager will be parsed with the given options directly. + /// + public Dictionary? MessagerOptions { get; set; } + /// + /// ParseMessagerOptionsByName parses messager options with both global-level and + /// messager-level options taken into consideration. + /// + public MessagerOptions ParseMessagerOptionsByName(string name) + { + var mopts = MessagerOptions?.TryGetValue(name, out var val) == true ? (MessagerOptions)val.Clone() : new MessagerOptions(); + mopts.IgnoreUnknownFields ??= IgnoreUnknownFields; + mopts.ReadFunc ??= ReadFunc; + mopts.LoadFunc ??= LoadFunc; + return mopts; + } + } + + /// + /// MessagerOptions defines the options for loading a messager. + /// + public class MessagerOptions : BaseOptions, ICloneable + { + /// + /// Path maps each messager name to a corresponding config file path. + /// If specified, then the main messager will be parsed from the file + /// directly, other than the specified load dir. + /// + public string? Path { get; set; } + + public object Clone() + { + return new MessagerOptions + { + IgnoreUnknownFields = IgnoreUnknownFields, + ReadFunc = ReadFunc, + LoadFunc = LoadFunc, + Path = Path + }; + } + } + + /// + /// LoadMessager loads a protobuf message from the specified file path and format. + /// + public static pb::IMessage? LoadMessager(pbr::MessageDescriptor desc, string path, Format fmt, in MessagerOptions? options = null) + { + var readFunc = options?.ReadFunc ?? File.ReadAllBytes; + byte[] content; + try + { + content = readFunc(path); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to read {path}: {ex.Message}"); + throw; + } + return Unmarshal(content, desc, fmt, options); + } + + /// + /// LoadMessagerInDir loads a protobuf message from the specified directory and format. + /// It resolves the file path based on the message descriptor name. + /// + public static pb::IMessage? LoadMessagerInDir(pbr::MessageDescriptor desc, string dir, Format fmt, in MessagerOptions? options = null) + { + string name = desc.Name; + string path = ""; + if (options?.Path != null) + { + path = options.Path; + fmt = Util.GetFormat(path); + } + if (path == "") + { + string filename = name + Util.Format2Ext(fmt); + path = Path.Combine(dir, filename); + } + var loadFunc = options?.LoadFunc ?? LoadMessager; + return loadFunc(desc, path, fmt, options); + } + + /// + /// Unmarshal parses the given byte content into a protobuf message based on the specified format. + /// + public static pb::IMessage? Unmarshal(byte[] content, pbr::MessageDescriptor desc, Format fmt, in MessagerOptions? options = null) + { + switch (fmt) + { + case Format.JSON: + try + { + var parser = new pb::JsonParser( + pb::JsonParser.Settings.Default.WithIgnoreUnknownFields(options?.IgnoreUnknownFields ?? false) + ); + return parser.Parse(new StreamReader(new MemoryStream(content)), desc); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to parse {desc.Name}.json: {ex.Message}"); + throw; + } + case Format.Bin: + try + { + return desc.Parser.ParseFrom(content); + } + catch (Exception ex) + { + Util.SetErrMsg($"failed to parse {desc.Name}.binpb: {ex.Message}"); + throw; + } + default: + Util.SetErrMsg($"unknown format: {fmt}"); + return null; + } + } + } + + /// + /// IMessagerName is an interface that provides the Name() method for messagers. + /// + public interface IMessagerName + { + string Name(); + } + + /// + /// Messager is the base class for all generated configuration messagers. + /// It is designed for three goals: + /// 1. Easy use: simple yet powerful accessors. + /// 2. Elegant API: concise and clean functions. + /// 3. Extensibility: Map, OrderedMap, Index, OrderedIndex... + /// + public abstract class Messager + { + /// + /// Stats contains statistics info about loading. + /// + public class Stats + { + /// Total load time consuming. + public TimeSpan Duration; + } + + protected Stats LoadStats = new(); + + /// + /// GetStats returns the loading stats info. + /// + public ref readonly Stats GetStats() => ref LoadStats; + + /// + /// Load fills message from file in the specified directory and format. + /// + public abstract bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null); + + /// + /// Message returns the inner protobuf message data. + /// + public virtual pb::IMessage? Message() => null; + + /// + /// ProcessAfterLoad is invoked after this messager loaded. + /// + protected virtual bool ProcessAfterLoad() => true; + + /// + /// ProcessAfterLoadAll is invoked after all messagers loaded. + /// + public virtual bool ProcessAfterLoadAll(in Hub hub) => true; + } +} diff --git a/test/csharp-tableau-loader/tableau/PatchConf.pc.cs b/test/csharp-tableau-loader/tableau/PatchConf.pc.cs new file mode 100644 index 00000000..6290448b --- /dev/null +++ b/test/csharp-tableau-loader/tableau/PatchConf.pc.cs @@ -0,0 +1,193 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// source: patch_conf.proto +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// PatchReplaceConf is a wrapper around protobuf message Protoconf.PatchReplaceConf. + /// + public class PatchReplaceConf : Messager, IMessagerName + { + private Protoconf.PatchReplaceConf _data = new(); + + /// + /// Name returns the PatchReplaceConf's message name. + /// + public string Name() => Protoconf.PatchReplaceConf.Descriptor.Name; + + /// + /// Load loads PatchReplaceConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.PatchReplaceConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.PatchReplaceConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load PatchReplaceConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the PatchReplaceConf's inner message data. + /// + public ref readonly Protoconf.PatchReplaceConf Data() => ref _data; + + /// + /// Message returns the PatchReplaceConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + } + + /// + /// PatchMergeConf is a wrapper around protobuf message Protoconf.PatchMergeConf. + /// + public class PatchMergeConf : Messager, IMessagerName + { + private Protoconf.PatchMergeConf _data = new(); + + /// + /// Name returns the PatchMergeConf's message name. + /// + public string Name() => Protoconf.PatchMergeConf.Descriptor.Name; + + /// + /// Load loads PatchMergeConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.PatchMergeConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.PatchMergeConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load PatchMergeConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the PatchMergeConf's inner message data. + /// + public ref readonly Protoconf.PatchMergeConf Data() => ref _data; + + /// + /// Message returns the PatchMergeConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Item? Get1(uint id) => + _data.ItemMap?.TryGetValue(id, out var val) == true ? val : null; + } + + /// + /// RecursivePatchConf is a wrapper around protobuf message Protoconf.RecursivePatchConf. + /// + public class RecursivePatchConf : Messager, IMessagerName + { + private Protoconf.RecursivePatchConf _data = new(); + + /// + /// Name returns the RecursivePatchConf's message name. + /// + public string Name() => Protoconf.RecursivePatchConf.Descriptor.Name; + + /// + /// Load loads RecursivePatchConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.RecursivePatchConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.RecursivePatchConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load RecursivePatchConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the RecursivePatchConf's inner message data. + /// + public ref readonly Protoconf.RecursivePatchConf Data() => ref _data; + + /// + /// Message returns the RecursivePatchConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.RecursivePatchConf.Types.Shop? Get1(uint shopId) => + _data.ShopMap?.TryGetValue(shopId, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.RecursivePatchConf.Types.Shop.Types.Goods? Get2(uint shopId, uint goodsId) => + Get1(shopId)?.GoodsMap?.TryGetValue(goodsId, out var val) == true ? val : null; + + /// + /// Get3 finds value in the 3rd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.RecursivePatchConf.Types.Shop.Types.Goods.Types.Currency? Get3(uint shopId, uint goodsId, uint type) => + Get2(shopId, goodsId)?.CurrencyMap?.TryGetValue(type, out var val) == true ? val : null; + + /// + /// Get4 finds value in the 4th-level map. + /// It will return null if the key is not found. + /// + public int? Get4(uint shopId, uint goodsId, uint type, int key4) => + Get3(shopId, goodsId, type)?.ValueList?.TryGetValue(key4, out var val) == true ? val : null; + } +} diff --git a/test/csharp-tableau-loader/tableau/TestConf.pc.cs b/test/csharp-tableau-loader/tableau/TestConf.pc.cs new file mode 100644 index 00000000..51404588 --- /dev/null +++ b/test/csharp-tableau-loader/tableau/TestConf.pc.cs @@ -0,0 +1,898 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// source: test_conf.proto +// +#nullable enable +using System; +using System.Collections.Generic; +using System.Linq; +using pb = global::Google.Protobuf; +namespace Tableau +{ + /// + /// ActivityConf is a wrapper around protobuf message Protoconf.ActivityConf. + /// + public class ActivityConf : Messager, IMessagerName + { + // OrderedMap types. + public class OrderedMap_int32Map : SortedDictionary { } + + public class OrderedMap_protoconf_SectionValue : Tuple + { + public OrderedMap_protoconf_SectionValue(OrderedMap_int32Map item1, Protoconf.Section item2) : base(item1, item2) { } + } + public class OrderedMap_protoconf_SectionMap : SortedDictionary { } + + public class OrderedMap_Activity_ChapterValue : Tuple + { + public OrderedMap_Activity_ChapterValue(OrderedMap_protoconf_SectionMap item1, Protoconf.ActivityConf.Types.Activity.Types.Chapter item2) : base(item1, item2) { } + } + public class OrderedMap_Activity_ChapterMap : SortedDictionary { } + + public class OrderedMap_ActivityValue : Tuple + { + public OrderedMap_ActivityValue(OrderedMap_Activity_ChapterMap item1, Protoconf.ActivityConf.Types.Activity item2) : base(item1, item2) { } + } + public class OrderedMap_ActivityMap : SortedDictionary { } + + private OrderedMap_ActivityMap _orderedMap = new OrderedMap_ActivityMap(); + + + // LevelIndex keys. + public readonly struct LevelIndex_Activity_ChapterKey : IEquatable + { + public ulong ActivityId { get; } + public uint ChapterId { get; } + + public LevelIndex_Activity_ChapterKey(ulong activityId, uint chapterId) + { + ActivityId = activityId; + ChapterId = chapterId; + } + + public bool Equals(LevelIndex_Activity_ChapterKey other) => + (ActivityId, ChapterId).Equals((other.ActivityId, other.ChapterId)); + + public override int GetHashCode() => + (ActivityId, ChapterId).GetHashCode(); + } + + // Index types. + // Index: ActivityName + public class Index_ActivityMap : Dictionary> { } + + private Index_ActivityMap _indexActivityMap = new Index_ActivityMap(); + + // Index: ChapterID + public class Index_ChapterMap : Dictionary> { } + + private Index_ChapterMap _indexChapterMap = new Index_ChapterMap(); + + private Dictionary _indexChapterMap1 = new Dictionary(); + + // Index: ChapterName@NamedChapter + public class Index_NamedChapterMap : Dictionary> { } + + private Index_NamedChapterMap _indexNamedChapterMap = new Index_NamedChapterMap(); + + private Dictionary _indexNamedChapterMap1 = new Dictionary(); + + // Index: SectionItemID@Award + public class Index_AwardMap : Dictionary> { } + + private Index_AwardMap _indexAwardMap = new Index_AwardMap(); + + private Dictionary _indexAwardMap1 = new Dictionary(); + + private Dictionary _indexAwardMap2 = new Dictionary(); + + private Protoconf.ActivityConf _data = new(); + + /// + /// Name returns the ActivityConf's message name. + /// + public string Name() => Protoconf.ActivityConf.Descriptor.Name; + + /// + /// Load loads ActivityConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.ActivityConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.ActivityConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load ActivityConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the ActivityConf's inner message data. + /// + public ref readonly Protoconf.ActivityConf Data() => ref _data; + + /// + /// Message returns the ActivityConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // OrderedMap init. + _orderedMap.Clear(); + foreach (var (key1, value1) in _data.ActivityMap) + { + var ordered_map1 = new OrderedMap_Activity_ChapterMap(); + foreach (var (key2, value2) in value1.ChapterMap) + { + var ordered_map2 = new OrderedMap_protoconf_SectionMap(); + foreach (var (key3, value3) in value2.SectionMap) + { + var ordered_map3 = new OrderedMap_int32Map(); + foreach (var (key4, value4) in value3.SectionRankMap) + { + ordered_map3[key4] = value4; + } + ordered_map2[key3] = new OrderedMap_protoconf_SectionValue(ordered_map3, value3); + } + ordered_map1[key2] = new OrderedMap_Activity_ChapterValue(ordered_map2, value2); + } + _orderedMap[key1] = new OrderedMap_ActivityValue(ordered_map1, value1); + } + // Index init. + _indexActivityMap.Clear(); + _indexChapterMap.Clear(); + _indexChapterMap1.Clear(); + _indexNamedChapterMap.Clear(); + _indexNamedChapterMap1.Clear(); + _indexAwardMap.Clear(); + _indexAwardMap1.Clear(); + _indexAwardMap2.Clear(); + foreach (var item1 in _data.ActivityMap) + { + var k1 = item1.Key; + { + // Index: ActivityName + var key = item1.Value.ActivityName; + { + var list = _indexActivityMap.TryGetValue(key, out var existingList) ? + existingList : _indexActivityMap[key] = new List(); + list.Add(item1.Value); + } + } + foreach (var item2 in item1.Value.ChapterMap) + { + var k2 = item2.Key; + { + // Index: ChapterID + var key = item2.Value.ChapterId; + { + var list = _indexChapterMap.TryGetValue(key, out var existingList) ? + existingList : _indexChapterMap[key] = new List(); + list.Add(item2.Value); + } + { + var map = _indexChapterMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexChapterMap1[k1] = new Index_ChapterMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item2.Value); + } + } + { + // Index: ChapterName@NamedChapter + var key = item2.Value.ChapterName; + { + var list = _indexNamedChapterMap.TryGetValue(key, out var existingList) ? + existingList : _indexNamedChapterMap[key] = new List(); + list.Add(item2.Value); + } + { + var map = _indexNamedChapterMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexNamedChapterMap1[k1] = new Index_NamedChapterMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item2.Value); + } + } + foreach (var item3 in item2.Value.SectionMap) + { + foreach (var item4 in item3.Value.SectionItemList) + { + { + // Index: SectionItemID@Award + var key = item4.Id; + { + var list = _indexAwardMap.TryGetValue(key, out var existingList) ? + existingList : _indexAwardMap[key] = new List(); + list.Add(item4); + } + { + var map = _indexAwardMap1.TryGetValue(k1, out var existingMap) ? + existingMap : _indexAwardMap1[k1] = new Index_AwardMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item4); + } + { + var mapKey = new LevelIndex_Activity_ChapterKey(k1, k2); + var map = _indexAwardMap2.TryGetValue(mapKey, out var existingMap) ? + existingMap : _indexAwardMap2[mapKey] = new Index_AwardMap(); + var list = map.TryGetValue(key, out var existingList) ? + existingList : map[key] = new List(); + list.Add(item4); + } + } + } + } + } + } + // Index(sort): ChapterName@NamedChapter + Comparison indexNamedChapterMapComparison = (a, b) => + (a.AwardId).CompareTo((b.AwardId)); + foreach (var itemList in _indexNamedChapterMap.Values) + { + itemList.Sort(indexNamedChapterMapComparison); + } + foreach (var itemDict in _indexNamedChapterMap1.Values) + { + foreach (var itemList in itemDict.Values) + { + itemList.Sort(indexNamedChapterMapComparison); + } + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.ActivityConf.Types.Activity? Get1(ulong activityId) => + _data.ActivityMap?.TryGetValue(activityId, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.ActivityConf.Types.Activity.Types.Chapter? Get2(ulong activityId, uint chapterId) => + Get1(activityId)?.ChapterMap?.TryGetValue(chapterId, out var val) == true ? val : null; + + /// + /// Get3 finds value in the 3rd-level map. + /// It will return null if the key is not found. + /// + public Protoconf.Section? Get3(ulong activityId, uint chapterId, uint sectionId) => + Get2(activityId, chapterId)?.SectionMap?.TryGetValue(sectionId, out var val) == true ? val : null; + + /// + /// Get4 finds value in the 4th-level map. + /// It will return null if the key is not found. + /// + public int? Get4(ulong activityId, uint chapterId, uint sectionId, uint key4) => + Get3(activityId, chapterId, sectionId)?.SectionRankMap?.TryGetValue(key4, out var val) == true ? val : null; + + // OrderedMap accessers. + /// + /// GetOrderedMap returns the 1st-level ordered map. + /// + public ref readonly OrderedMap_ActivityMap GetOrderedMap() => ref _orderedMap; + + /// + /// GetOrderedMap1 finds value in the 1st-level ordered map. + /// It will return null if the key is not found. + /// + public OrderedMap_Activity_ChapterMap? GetOrderedMap1(ulong activityId) => + _orderedMap.TryGetValue(activityId, out var value) ? value.Item1 : null; + + /// + /// GetOrderedMap2 finds value in the 2nd-level ordered map. + /// It will return null if the key is not found. + /// + public OrderedMap_protoconf_SectionMap? GetOrderedMap2(ulong activityId, uint chapterId) => + GetOrderedMap1(activityId)?.TryGetValue(chapterId, out var value) == true ? value.Item1 : null; + + /// + /// GetOrderedMap3 finds value in the 3rd-level ordered map. + /// It will return null if the key is not found. + /// + public OrderedMap_int32Map? GetOrderedMap3(ulong activityId, uint chapterId, uint sectionId) => + GetOrderedMap2(activityId, chapterId)?.TryGetValue(sectionId, out var value) == true ? value.Item1 : null; + + // Index: ActivityName + + /// + /// FindActivityMap finds the index: key(ActivityName) to value(Protoconf.ActivityConf.Types.Activity) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ActivityMap FindActivityMap() => ref _indexActivityMap; + + /// + /// FindActivity finds a list of all values of the given key(s). + /// + public List? FindActivity(string activityName) => + _indexActivityMap.TryGetValue(activityName, out var value) ? value : null; + + /// + /// FindFirstActivity finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ActivityConf.Types.Activity? FindFirstActivity(string activityName) => + FindActivity(activityName)?.FirstOrDefault(); + + // Index: ChapterID + + /// + /// FindChapterMap finds the index: key(ChapterID) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_ChapterMap FindChapterMap() => ref _indexChapterMap; + + /// + /// FindChapter finds a list of all values of the given key(s). + /// + public List? FindChapter(uint chapterId) => + _indexChapterMap.TryGetValue(chapterId, out var value) ? value : null; + + /// + /// FindFirstChapter finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstChapter(uint chapterId) => + FindChapter(chapterId)?.FirstOrDefault(); + + /// + /// FindChapterMap1 finds the index: key(ChapterID) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter), + /// which is the upper 1st-level map specified by (activityId). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_ChapterMap? FindChapterMap1(ulong activityId) => + _indexChapterMap1.TryGetValue(activityId, out var value) ? value : null; + + /// + /// FindChapter1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (activityId). + /// + public List? FindChapter1(ulong activityId, uint chapterId) => + FindChapterMap1(activityId)?.TryGetValue(chapterId, out var value) == true ? value : null; + + /// + /// FindFirstChapter1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (activityId), or null if no value found. + /// + public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstChapter1(ulong activityId, uint chapterId) => + FindChapter1(activityId, chapterId)?.FirstOrDefault(); + + // Index: ChapterName@NamedChapter + + /// + /// FindNamedChapterMap finds the index: key(ChapterName@NamedChapter) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_NamedChapterMap FindNamedChapterMap() => ref _indexNamedChapterMap; + + /// + /// FindNamedChapter finds a list of all values of the given key(s). + /// + public List? FindNamedChapter(string chapterName) => + _indexNamedChapterMap.TryGetValue(chapterName, out var value) ? value : null; + + /// + /// FindFirstNamedChapter finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstNamedChapter(string chapterName) => + FindNamedChapter(chapterName)?.FirstOrDefault(); + + /// + /// FindNamedChapterMap1 finds the index: key(ChapterName@NamedChapter) to value(Protoconf.ActivityConf.Types.Activity.Types.Chapter), + /// which is the upper 1st-level map specified by (activityId). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_NamedChapterMap? FindNamedChapterMap1(ulong activityId) => + _indexNamedChapterMap1.TryGetValue(activityId, out var value) ? value : null; + + /// + /// FindNamedChapter1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (activityId). + /// + public List? FindNamedChapter1(ulong activityId, string chapterName) => + FindNamedChapterMap1(activityId)?.TryGetValue(chapterName, out var value) == true ? value : null; + + /// + /// FindFirstNamedChapter1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (activityId), or null if no value found. + /// + public Protoconf.ActivityConf.Types.Activity.Types.Chapter? FindFirstNamedChapter1(ulong activityId, string chapterName) => + FindNamedChapter1(activityId, chapterName)?.FirstOrDefault(); + + // Index: SectionItemID@Award + + /// + /// FindAwardMap finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_AwardMap FindAwardMap() => ref _indexAwardMap; + + /// + /// FindAward finds a list of all values of the given key(s). + /// + public List? FindAward(uint id) => + _indexAwardMap.TryGetValue(id, out var value) ? value : null; + + /// + /// FindFirstAward finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.Section.Types.SectionItem? FindFirstAward(uint id) => + FindAward(id)?.FirstOrDefault(); + + /// + /// FindAwardMap1 finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem), + /// which is the upper 1st-level map specified by (activityId). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_AwardMap? FindAwardMap1(ulong activityId) => + _indexAwardMap1.TryGetValue(activityId, out var value) ? value : null; + + /// + /// FindAward1 finds a list of all values of the given key(s) in the upper 1st-level map + /// specified by (activityId). + /// + public List? FindAward1(ulong activityId, uint id) => + FindAwardMap1(activityId)?.TryGetValue(id, out var value) == true ? value : null; + + /// + /// FindFirstAward1 finds the first value of the given key(s) in the upper 1st-level map + /// specified by (activityId), or null if no value found. + /// + public Protoconf.Section.Types.SectionItem? FindFirstAward1(ulong activityId, uint id) => + FindAward1(activityId, id)?.FirstOrDefault(); + + /// + /// FindAwardMap2 finds the index: key(SectionItemID@Award) to value(Protoconf.Section.Types.SectionItem), + /// which is the upper 2nd-level map specified by (activityId, chapterId). + /// One key may correspond to multiple values, which are represented by a list. + /// + public Index_AwardMap? FindAwardMap2(ulong activityId, uint chapterId) => + _indexAwardMap2.TryGetValue(new LevelIndex_Activity_ChapterKey(activityId, chapterId), out var value) ? value : null; + + /// + /// FindAward2 finds a list of all values of the given key(s) in the upper 2nd-level map + /// specified by (activityId, chapterId). + /// + public List? FindAward2(ulong activityId, uint chapterId, uint id) => + FindAwardMap2(activityId, chapterId)?.TryGetValue(id, out var value) == true ? value : null; + + /// + /// FindFirstAward2 finds the first value of the given key(s) in the upper 2nd-level map + /// specified by (activityId, chapterId), or null if no value found. + /// + public Protoconf.Section.Types.SectionItem? FindFirstAward2(ulong activityId, uint chapterId, uint id) => + FindAward2(activityId, chapterId, id)?.FirstOrDefault(); + } + + /// + /// ChapterConf is a wrapper around protobuf message Protoconf.ChapterConf. + /// + public class ChapterConf : Messager, IMessagerName + { + private Protoconf.ChapterConf _data = new(); + + /// + /// Name returns the ChapterConf's message name. + /// + public string Name() => Protoconf.ChapterConf.Descriptor.Name; + + /// + /// Load loads ChapterConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.ChapterConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.ChapterConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load ChapterConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the ChapterConf's inner message data. + /// + public ref readonly Protoconf.ChapterConf Data() => ref _data; + + /// + /// Message returns the ChapterConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.ChapterConf.Types.Chapter? Get1(ulong id) => + _data.ChapterMap?.TryGetValue(id, out var val) == true ? val : null; + } + + /// + /// ThemeConf is a wrapper around protobuf message Protoconf.ThemeConf. + /// + public class ThemeConf : Messager, IMessagerName + { + private Protoconf.ThemeConf _data = new(); + + /// + /// Name returns the ThemeConf's message name. + /// + public string Name() => Protoconf.ThemeConf.Descriptor.Name; + + /// + /// Load loads ThemeConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.ThemeConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.ThemeConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load ThemeConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the ThemeConf's inner message data. + /// + public ref readonly Protoconf.ThemeConf Data() => ref _data; + + /// + /// Message returns the ThemeConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.ThemeConf.Types.Theme? Get1(string name) => + _data.ThemeMap?.TryGetValue(name, out var val) == true ? val : null; + + /// + /// Get2 finds value in the 2nd-level map. + /// It will return null if the key is not found. + /// + public string? Get2(string name, string param) => + Get1(name)?.ParamMap?.TryGetValue(param, out var val) == true ? val : null; + } + + /// + /// TaskConf is a wrapper around protobuf message Protoconf.TaskConf. + /// + public class TaskConf : Messager, IMessagerName + { + // Index types. + // Index: ActivityID + public class Index_TaskMap : Dictionary> { } + + private Index_TaskMap _indexTaskMap = new Index_TaskMap(); + + // OrderedIndex types. + // OrderedIndex: Goal@OrderedTask + public class OrderedIndex_OrderedTaskMap : SortedDictionary> { } + + private OrderedIndex_OrderedTaskMap _orderedIndexOrderedTaskMap = new OrderedIndex_OrderedTaskMap(); + + // OrderedIndex: Expiry@TaskExpiry + public class OrderedIndex_TaskExpiryMap : SortedDictionary> { } + + private OrderedIndex_TaskExpiryMap _orderedIndexTaskExpiryMap = new OrderedIndex_TaskExpiryMap(); + + // OrderedIndex: Expiry@SortedTaskExpiry + public class OrderedIndex_SortedTaskExpiryMap : SortedDictionary> { } + + private OrderedIndex_SortedTaskExpiryMap _orderedIndexSortedTaskExpiryMap = new OrderedIndex_SortedTaskExpiryMap(); + + // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry + public readonly struct OrderedIndex_ActivityExpiryKey : IComparable + { + public long Expiry { get; } + public long ActivityId { get; } + + public OrderedIndex_ActivityExpiryKey(long expiry, long activityId) + { + Expiry = expiry; + ActivityId = activityId; + } + + public int CompareTo(OrderedIndex_ActivityExpiryKey other) => + (Expiry, ActivityId).CompareTo((other.Expiry, other.ActivityId)); + } + + public class OrderedIndex_ActivityExpiryMap : SortedDictionary> { } + + private OrderedIndex_ActivityExpiryMap _orderedIndexActivityExpiryMap = new OrderedIndex_ActivityExpiryMap(); + + private Protoconf.TaskConf _data = new(); + + /// + /// Name returns the TaskConf's message name. + /// + public string Name() => Protoconf.TaskConf.Descriptor.Name; + + /// + /// Load loads TaskConf's content in the given dir, based on format and messager options. + /// + public override bool Load(string dir, Format fmt, in Load.MessagerOptions? options = null) + { + var start = DateTime.Now; + try + { + _data = (Protoconf.TaskConf)( + Tableau.Load.LoadMessagerInDir(Protoconf.TaskConf.Descriptor, dir, fmt, options) + ?? throw new InvalidOperationException() + ); + } + catch (Exception ex) + { + if (string.IsNullOrEmpty(Util.GetErrMsg())) + { + Util.SetErrMsg($"failed to load TaskConf: {ex.Message}"); + } + return false; + } + LoadStats.Duration = DateTime.Now - start; + return ProcessAfterLoad(); + } + + /// + /// Data returns the TaskConf's inner message data. + /// + public ref readonly Protoconf.TaskConf Data() => ref _data; + + /// + /// Message returns the TaskConf's inner message data. + /// + public override pb::IMessage? Message() => _data; + + /// + /// ProcessAfterLoad runs after this messager is loaded. + /// + protected override bool ProcessAfterLoad() + { + // Index init. + _indexTaskMap.Clear(); + foreach (var item1 in _data.TaskMap) + { + { + // Index: ActivityID + var key = item1.Value.ActivityId; + { + var list = _indexTaskMap.TryGetValue(key, out var existingList) ? + existingList : _indexTaskMap[key] = new List(); + list.Add(item1.Value); + } + } + } + // Index(sort): ActivityID + Comparison indexTaskMapComparison = (a, b) => + (a.Goal, a.Id).CompareTo((b.Goal, b.Id)); + foreach (var itemList in _indexTaskMap.Values) + { + itemList.Sort(indexTaskMapComparison); + } + // OrderedIndex init. + _orderedIndexOrderedTaskMap.Clear(); + _orderedIndexTaskExpiryMap.Clear(); + _orderedIndexSortedTaskExpiryMap.Clear(); + _orderedIndexActivityExpiryMap.Clear(); + foreach (var item1 in _data.TaskMap) + { + { + // OrderedIndex: Goal@OrderedTask + var key = item1.Value.Goal; + { + var list = _orderedIndexOrderedTaskMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexOrderedTaskMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // OrderedIndex: Expiry@TaskExpiry + var key = item1.Value.Expiry?.Seconds ?? 0; + { + var list = _orderedIndexTaskExpiryMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexTaskExpiryMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // OrderedIndex: Expiry@SortedTaskExpiry + var key = item1.Value.Expiry?.Seconds ?? 0; + { + var list = _orderedIndexSortedTaskExpiryMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexSortedTaskExpiryMap[key] = new List(); + list.Add(item1.Value); + } + } + { + // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry + var key = new OrderedIndex_ActivityExpiryKey(item1.Value.Expiry?.Seconds ?? 0, item1.Value.ActivityId); + { + var list = _orderedIndexActivityExpiryMap.TryGetValue(key, out var existingList) ? + existingList : _orderedIndexActivityExpiryMap[key] = new List(); + list.Add(item1.Value); + } + } + } + // OrderedIndex(sort): Goal@OrderedTask + Comparison orderedIndexOrderedTaskMapComparison = (a, b) => + (a.Id).CompareTo((b.Id)); + foreach (var itemList in _orderedIndexOrderedTaskMap.Values) + { + itemList.Sort(orderedIndexOrderedTaskMapComparison); + } + // OrderedIndex(sort): Expiry@SortedTaskExpiry + Comparison orderedIndexSortedTaskExpiryMapComparison = (a, b) => + (a.Goal, a.Id).CompareTo((b.Goal, b.Id)); + foreach (var itemList in _orderedIndexSortedTaskExpiryMap.Values) + { + itemList.Sort(orderedIndexSortedTaskExpiryMapComparison); + } + return true; + } + + /// + /// Get1 finds value in the 1st-level map. + /// It will return null if the key is not found. + /// + public Protoconf.TaskConf.Types.Task? Get1(long id) => + _data.TaskMap?.TryGetValue(id, out var val) == true ? val : null; + + // Index: ActivityID + + /// + /// FindTaskMap finds the index: key(ActivityID) to value(Protoconf.TaskConf.Types.Task) map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly Index_TaskMap FindTaskMap() => ref _indexTaskMap; + + /// + /// FindTask finds a list of all values of the given key(s). + /// + public List? FindTask(long activityId) => + _indexTaskMap.TryGetValue(activityId, out var value) ? value : null; + + /// + /// FindFirstTask finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.TaskConf.Types.Task? FindFirstTask(long activityId) => + FindTask(activityId)?.FirstOrDefault(); + + // OrderedIndex: Goal@OrderedTask + + /// + /// FindOrderedTaskMap finds the ordered index: key(Goal@OrderedTask) to value(Protoconf.TaskConf.Types.Task) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_OrderedTaskMap FindOrderedTaskMap() => ref _orderedIndexOrderedTaskMap; + + /// + /// FindOrderedTask finds a list of all values of the given key(s). + /// + public List? FindOrderedTask(long goal) => + _orderedIndexOrderedTaskMap.TryGetValue(goal, out var value) ? value : null; + + /// + /// FindFirstOrderedTask finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.TaskConf.Types.Task? FindFirstOrderedTask(long goal) => + FindOrderedTask(goal)?.FirstOrDefault(); + + // OrderedIndex: Expiry@TaskExpiry + + /// + /// FindTaskExpiryMap finds the ordered index: key(Expiry@TaskExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_TaskExpiryMap FindTaskExpiryMap() => ref _orderedIndexTaskExpiryMap; + + /// + /// FindTaskExpiry finds a list of all values of the given key(s). + /// + public List? FindTaskExpiry(long expiry) => + _orderedIndexTaskExpiryMap.TryGetValue(expiry, out var value) ? value : null; + + /// + /// FindFirstTaskExpiry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.TaskConf.Types.Task? FindFirstTaskExpiry(long expiry) => + FindTaskExpiry(expiry)?.FirstOrDefault(); + + // OrderedIndex: Expiry@SortedTaskExpiry + + /// + /// FindSortedTaskExpiryMap finds the ordered index: key(Expiry@SortedTaskExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_SortedTaskExpiryMap FindSortedTaskExpiryMap() => ref _orderedIndexSortedTaskExpiryMap; + + /// + /// FindSortedTaskExpiry finds a list of all values of the given key(s). + /// + public List? FindSortedTaskExpiry(long expiry) => + _orderedIndexSortedTaskExpiryMap.TryGetValue(expiry, out var value) ? value : null; + + /// + /// FindFirstSortedTaskExpiry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.TaskConf.Types.Task? FindFirstSortedTaskExpiry(long expiry) => + FindSortedTaskExpiry(expiry)?.FirstOrDefault(); + + // OrderedIndex: (Expiry,ActivityID)@ActivityExpiry + + /// + /// FindActivityExpiryMap finds the ordered index: key((Expiry,ActivityID)@ActivityExpiry) to value(Protoconf.TaskConf.Types.Task) sorted map. + /// One key may correspond to multiple values, which are represented by a list. + /// + public ref readonly OrderedIndex_ActivityExpiryMap FindActivityExpiryMap() => ref _orderedIndexActivityExpiryMap; + + /// + /// FindActivityExpiry finds a list of all values of the given key(s). + /// + public List? FindActivityExpiry(long expiry, long activityId) => + _orderedIndexActivityExpiryMap.TryGetValue(new OrderedIndex_ActivityExpiryKey(expiry, activityId), out var value) ? value : null; + + /// + /// FindFirstActivityExpiry finds the first value of the given key(s), + /// or null if no value found. + /// + public Protoconf.TaskConf.Types.Task? FindFirstActivityExpiry(long expiry, long activityId) => + FindActivityExpiry(expiry, activityId)?.FirstOrDefault(); + } +} diff --git a/test/csharp-tableau-loader/tableau/Util.pc.cs b/test/csharp-tableau-loader/tableau/Util.pc.cs new file mode 100644 index 00000000..c907729d --- /dev/null +++ b/test/csharp-tableau-loader/tableau/Util.pc.cs @@ -0,0 +1,70 @@ +// +// Code generated by protoc-gen-csharp-tableau-loader. DO NOT EDIT. +// versions: +// - protoc-gen-csharp-tableau-loader v0.1.0 +// - protoc v3.19.3 +// +#nullable enable +using System; +using System.IO; +namespace Tableau +{ + /// + /// Format specifies the format of the configuration file. + /// + public enum Format + { + Unknown, + JSON, + Bin + } + + /// + /// Util provides common utility functions. + /// + public static class Util + { + [ThreadStatic] private static string? _errMsg; + + /// + /// Get the last error message. + /// + public static string GetErrMsg() => _errMsg ?? ""; + + /// + /// Set the error message. + /// + public static void SetErrMsg(string msg) => _errMsg = msg; + + private const string _unknownExt = ".unknown"; + private const string _jsonExt = ".json"; + private const string _binExt = ".binpb"; + + /// + /// GetFormat returns the Format type determined by the file extension of the given path. + /// + public static Format GetFormat(string path) + { + string ext = Path.GetExtension(path); + return ext switch + { + _jsonExt => Format.JSON, + _binExt => Format.Bin, + _ => Format.Unknown, + }; + } + + /// + /// Format2Ext returns the file extension corresponding to the given format. + /// + public static string Format2Ext(Format fmt) + { + return fmt switch + { + Format.JSON => _jsonExt, + Format.Bin => _binExt, + _ => _unknownExt, + }; + } + } +} diff --git a/test/proto/hero_conf.proto b/test/proto/hero_conf.proto index 2437a682..ba636f4b 100644 --- a/test/proto/hero_conf.proto +++ b/test/proto/hero_conf.proto @@ -16,7 +16,7 @@ message HeroConf { ordered_map: true index: "Title" lang_options: { key: "Index" value: "go" } - lang_options: { key: "OrderedMap" value: "cpp" } + lang_options: { key: "OrderedMap" value: "cpp cs" } }; map hero_map = 1 [(tableau.field) = { key: "Name" layout: LAYOUT_VERTICAL }]; message Hero { diff --git a/test/testdata/bin/HeroConf.binpb b/test/testdata/bin/HeroConf.binpb new file mode 100644 index 00000000..58e8445a --- /dev/null +++ b/test/testdata/bin/HeroConf.binpb @@ -0,0 +1,11 @@ + ++ +venus" +venus +title2 +title2attr2 +) +zeus! +zeus +title1 +title1attr1 \ No newline at end of file diff --git a/test/testdata/conf/ItemConf.json b/test/testdata/conf/ItemConf.json index a2b6e2c5..bc09a013 100644 --- a/test/testdata/conf/ItemConf.json +++ b/test/testdata/conf/ItemConf.json @@ -14,6 +14,11 @@ }, "expiry": "2021-01-01T18:00:00Z", "type": "FRUIT_TYPE_APPLE", + "param_list": [ + 1, + 2, + 3 + ], "extTypeList": [ "FRUIT_TYPE_APPLE", "FRUIT_TYPE_ORANGE" @@ -29,7 +34,12 @@ ] }, "expiry": "2022-01-01T08:00:00Z", - "type": "FRUIT_TYPE_ORANGE" + "type": "FRUIT_TYPE_ORANGE", + "param_list": [ + 4, + 5, + 6 + ] }, "3": { "id": 3, @@ -40,7 +50,15 @@ "icon.png" ] }, - "expiry": "2023-01-01T08:00:00Z" + "expiry": "2023-01-01T08:00:00Z", + "param_list": [ + 1, + 2, + 3, + 4, + 5, + 6 + ] }, "2001": { "id": 2001,