refactor: 导表工具重构,增加部分特性,修复部分问题

1、增加测试用例;
2、支持多文件合并导表;
3、支持 "#" 开头忽略;
4、修复越界问题;
5、优化模板样式,增加模板规则说明;
This commit is contained in:
kercylan98 2023-07-01 16:07:30 +08:00
parent 73cefc9b48
commit afdda793bc
22 changed files with 268 additions and 313 deletions

View File

@ -36,9 +36,12 @@ func Init(loadDir string, loadHandle LoadHandle, refreshHandle RefreshHandle) {
cLoadDir = loadDir
cLoadHandle = loadHandle
cRefreshHandle = refreshHandle
Load()
Refresh()
}
// Load 加载配置
// - 加载后并不会刷新线上配置,需要执行 Refresh 函数对线上配置进行刷新
func Load() {
mutex.Lock()
if cTicker != nil {

View File

@ -1,14 +0,0 @@
// Code generated by minotaur-config-export. DO NOT EDIT.
package configs
// SystemConfig 系统
type SystemConfig struct {
Addr string // 监听地址
Finish string // 启动完成
}
// WelcomeConfig 欢迎词
type WelcomeConfig struct {
Id int // ID
Text string // 内容
}

View File

@ -1,47 +0,0 @@
// Code generated by minotaur-config-export. DO NOT EDIT.
package configs
import (
jsonIter "github.com/json-iterator/go"
"github.com/kercylan98/minotaur/utils/log"
"go.uber.org/zap"
"os"
)
var json = jsonIter.ConfigCompatibleWithStandardLibrary
var (
ISystemConfig *SystemConfig
iSystemConfig *SystemConfig
IWelcomeConfig map[int]*WelcomeConfig
iWelcomeConfig map[int]*WelcomeConfig
)
func LoadConfig(handle func(filename string, config any) error) {
var err error
iSystemConfig = new(SystemConfig)
if err = handle("SystemConfig.json", iSystemConfig); err != nil {
log.Error("Config", zap.String("Name", "SystemConfig"), zap.Bool("Invalid", true), zap.Error(err))
}
iWelcomeConfig = make(map[int]*WelcomeConfig)
if err = handle("WelcomeConfig.json", &iWelcomeConfig); err != nil {
log.Error("Config", zap.String("Name", "WelcomeConfig"), zap.Bool("Invalid", true), zap.Error(err))
}
}
func Refresh() {
ISystemConfig = iSystemConfig
IWelcomeConfig = iWelcomeConfig
}
func DefaultLoad(filepath string) {
LoadConfig(func(filename string, config any) error {
bytes, err := os.ReadFile(filepath)
if err != nil {
return err
}
return json.Unmarshal(bytes, &config)
})
}

View File

@ -1,4 +0,0 @@
{
"Finish": "StartFinish",
"Addr": ":9999"
}

View File

@ -1,10 +0,0 @@
{
"1": {
"Id": 1,
"Text": "Hello"
},
"2": {
"Id": 2,
"Text": "World"
}
}

View File

@ -1,30 +0,0 @@
// 该案例演示了配置导表工具的使用,其中包括了作为模板的配置文件及导出的配置文件
package main
import (
"github.com/kercylan98/minotaur/config"
"github.com/kercylan98/minotaur/examples/simple-server-config/config/configs"
"github.com/kercylan98/minotaur/planner/configexport"
"github.com/kercylan98/minotaur/utils/log"
"go.uber.org/zap"
"path/filepath"
)
const (
workdir = "./examples/simple-server-config"
)
func main() {
export()
config.Init(filepath.Join(workdir, "config", "json"), configs.LoadConfig, configs.Refresh)
config.Load()
config.Refresh()
log.Info("Config", zap.Any("SystemConfig", configs.ISystemConfig))
log.Info("Config", zap.Any("WelcomeConfig", configs.IWelcomeConfig))
}
func export() {
c := configexport.New(filepath.Join(workdir, "config", "系统配置.xlsx"))
c.ExportGo("", filepath.Join(workdir, "config", "configs"))
c.ExportServer("", filepath.Join(workdir, "config", "json"))
}

View File

@ -5,24 +5,52 @@ import (
"fmt"
"github.com/kercylan98/minotaur/planner/configexport/internal"
"github.com/kercylan98/minotaur/utils/file"
"github.com/kercylan98/minotaur/utils/log"
"github.com/kercylan98/minotaur/utils/str"
"github.com/tealeg/xlsx"
"go.uber.org/zap"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
// New 创建一个导表配置
func New(xlsxPath string) *ConfigExport {
ce := &ConfigExport{xlsxPath: xlsxPath}
ce := &ConfigExport{xlsxPath: xlsxPath, exist: make(map[string]bool)}
xlsxFile, err := xlsx.OpenFile(xlsxPath)
if err != nil {
panic(err)
}
for i := 0; i < len(xlsxFile.Sheets); i++ {
if config, err := internal.NewConfig(xlsxFile.Sheets[i]); err != nil {
panic(err)
sheet := xlsxFile.Sheets[i]
if config, err := internal.NewConfig(sheet, ce.exist); err != nil {
switch err {
case internal.ErrReadConfigFailedSame:
log.Warn("ConfigExport",
zap.String("File", xlsxPath),
zap.String("Sheet", sheet.Name),
zap.String("Info", "A configuration with the same name exists, skipped"),
)
case internal.ErrReadConfigFailedIgnore:
log.Info("ConfigExport",
zap.String("File", xlsxPath),
zap.String("Sheet", sheet.Name),
zap.String("Info", "Excluded non-configuration table files"),
)
default:
log.ErrorHideStack("ConfigExport",
zap.String("File", xlsxPath),
zap.String("Sheet", sheet.Name),
zap.String("Info", "Excluded non-configuration table files"),
)
}
} else {
if config == nil {
continue
}
ce.configs = append(ce.configs, config)
ce.exist[config.Name] = true
}
}
return ce
@ -31,6 +59,34 @@ func New(xlsxPath string) *ConfigExport {
type ConfigExport struct {
xlsxPath string
configs []*internal.Config
exist map[string]bool
}
// Merge 合并多个导表配置
func Merge(exports ...*ConfigExport) *ConfigExport {
if len(exports) == 0 {
return nil
}
if len(exports) == 1 {
return exports[0]
}
var export = exports[0]
for i := 1; i < len(exports); i++ {
ce := exports[i]
for _, config := range ce.configs {
if _, ok := export.exist[config.Name]; ok {
log.Warn("ConfigExport",
zap.String("File", ce.xlsxPath),
zap.String("Sheet", config.Name),
zap.String("Info", "A configuration with the same name exists, skipped"),
)
continue
}
export.configs = append(export.configs, config)
export.exist[config.Name] = true
}
}
return export
}
func (slf *ConfigExport) ExportClient(prefix, outputDir string) {
@ -99,9 +155,13 @@ func (slf *ConfigExport) exportGoConfig(outputDir string) {
return nil
})
if err := file.WriterFile(filepath.Join(outputDir, "config.go"), []byte(result)); err != nil {
filePath := filepath.Join(outputDir, "config.go")
if err := file.WriterFile(filePath, []byte(result)); err != nil {
panic(err)
}
cmd := exec.Command("gofmt", "-w", filePath)
_ = cmd.Run()
}
func (slf *ConfigExport) exportGoDefine(outputDir string) {
@ -136,7 +196,11 @@ func (slf *ConfigExport) exportGoDefine(outputDir string) {
return nil
})
if err := file.WriterFile(filepath.Join(outputDir, "config.define.go"), []byte(result)); err != nil {
filePath := filepath.Join(outputDir, "config.define.go")
if err := file.WriterFile(filePath, []byte(result)); err != nil {
panic(err)
}
cmd := exec.Command("gofmt", "-w", filePath)
_ = cmd.Run()
}

View File

@ -0,0 +1,42 @@
package configexport_test
import (
"fmt"
"github.com/kercylan98/minotaur/config"
"github.com/kercylan98/minotaur/planner/configexport"
"github.com/kercylan98/minotaur/planner/configexport/example"
"os"
"path/filepath"
"strings"
)
func ExampleNew() {
var workdir = "./"
files, err := os.ReadDir(workdir)
if err != nil {
panic(err)
}
var ces []*configexport.ConfigExport
for _, file := range files {
if file.IsDir() || !strings.HasSuffix(file.Name(), ".xlsx") || strings.HasPrefix(file.Name(), "~") {
continue
}
ces = append(ces, configexport.New(filepath.Join(workdir, file.Name())))
}
c := configexport.Merge(ces...)
outDir := filepath.Join(workdir, "example")
c.ExportGo("", outDir)
c.ExportServer("", outDir)
c.ExportClient("", outDir)
// 下方为配置加载代码
// 使用生成的 LoadConfig 函数加载配置
config.Init(outDir, example.LoadConfig, example.Refresh)
fmt.Println("success")
// Output:
// success
}

View File

@ -1,11 +0,0 @@
package configexport
import "testing"
func TestNew(t *testing.T) {
c := New(`./template.xlsx`)
c.ExportGo("server", "./example")
c.ExportClient("client", "./example")
c.ExportServer("server", "./example")
}

View File

@ -1,18 +1,17 @@
{
"Other": {
"1": {
"id": 2,
"name": "刘备"
},
"0": {
"id": 1,
"name": "张飞"
}
},
"Id": 1,
"Count": "a",
"Award": {
"0": "asd",
"1": "12"
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
},
"1": {
"id": 2,
"name": "刘备"
}
}
}

View File

@ -1,22 +1,22 @@
{
"1": {
"b": {
"Id": 1,
"Count": "b",
"Award": {
"0": "asd",
"1": "12"
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
"name": "张飞",
"id": 1
},
"1": {
"id": 2,
"name": "刘备"
"name": "刘备",
"id": 2
}
}
},
"Id": 1,
"Count": "b"
}
},
"2": {
@ -27,12 +27,12 @@
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
"name": "张飞",
"id": 1
},
"1": {
"name": "刘备",
"id": 2
"id": 2,
"name": "刘备"
}
},
"Id": 2,
@ -42,8 +42,8 @@
"Id": 2,
"Count": "d",
"Award": {
"0": "asd",
"1": "12"
"1": "12",
"0": "asd"
},
"Other": {
"0": {

View File

@ -1,60 +1,75 @@
// Code generated by minotaur-config-export. DO NOT EDIT.
package example
// IndexConfig 有索引
type IndexConfig struct {
Id int // 任务ID
Count string // 次数
Info *IndexConfigInfo // 信息
// IndexConfigDefine 有索引
type IndexConfigDefine struct {
Id int // 任务ID
Count string // 次数
Info *IndexConfigInfo // 信息
Other map[int]*IndexConfigOther // 信息2
}
}
func (slf *IndexConfigDefine) String() string {
if data, err := json.Marshal(slf); err == nil {
return string(data)
}
return "{}"
}
type IndexConfigInfo struct {
Id int
Id int
Name string
Info *IndexConfigInfoInfo
}
}
type IndexConfigInfoInfo struct {
Lv int
Lv int
Exp *IndexConfigInfoInfoExp
}
}
type IndexConfigInfoInfoExp struct {
Mux int
Mux int
Count int
}
}
type IndexConfigOther struct {
Id int
Id int
Name string
}
}
// EasyConfig 无索引
type EasyConfig struct {
Id int // 任务ID
Count string // 次数
Info *EasyConfigInfo // 信息
// EasyConfigDefine 无索引
type EasyConfigDefine struct {
Id int // 任务ID
Info *EasyConfigInfo // 信息
Other map[int]*EasyConfigOther // 信息2
}
}
func (slf *EasyConfigDefine) String() string {
if data, err := json.Marshal(slf); err == nil {
return string(data)
}
return "{}"
}
type EasyConfigInfo struct {
Id int
Id int
Name string
Info *EasyConfigInfoInfo
}
}
type EasyConfigInfoInfo struct {
Lv int
Lv int
Exp *EasyConfigInfoInfoExp
}
}
type EasyConfigInfoInfoExp struct {
Mux int
Mux int
Count int
}
}
type EasyConfigOther struct {
Id int
Id int
Name string
}
}

View File

@ -1,5 +1,6 @@
// Code generated by minotaur-config-export. DO NOT EDIT.
package example
import (
jsonIter "github.com/json-iterator/go"
"github.com/kercylan98/minotaur/utils/log"
@ -9,39 +10,40 @@ import (
var json = jsonIter.ConfigCompatibleWithStandardLibrary
var (
IIndexConfig map[int]map[string]*IndexConfig
iIndexConfig map[int]map[string]*IndexConfig
IEasyConfig *EasyConfig
iEasyConfig *EasyConfig
// IndexConfig 有索引
IndexConfig map[int]map[string]*IndexConfigDefine
_IndexConfig map[int]map[string]*IndexConfigDefine
// EasyConfig 无索引
EasyConfig *EasyConfigDefine
_EasyConfig *EasyConfigDefine
)
func LoadConfig(handle func(filename string, config any) error) {
var err error
iIndexConfig = make(map[int]map[string]*IndexConfig)
if err = handle("server.IndexConfig.json", &iIndexConfig); err != nil {
_IndexConfig = make(map[int]map[string]*IndexConfigDefine)
if err = handle("IndexConfig.json", &_IndexConfig); err != nil {
log.Error("Config", zap.String("Name", "IndexConfig"), zap.Bool("Invalid", true), zap.Error(err))
}
iEasyConfig = new(EasyConfig)
if err = handle("server.EasyConfig.json", iEasyConfig); err != nil {
_EasyConfig = new(EasyConfigDefine)
if err = handle("EasyConfig.json", _EasyConfig); err != nil {
log.Error("Config", zap.String("Name", "EasyConfig"), zap.Bool("Invalid", true), zap.Error(err))
}
}
func Refresh() {
IIndexConfig = iIndexConfig
IEasyConfig = iEasyConfig
IndexConfig = _IndexConfig
EasyConfig = _EasyConfig
}
func DefaultLoad(filepath string) {
LoadConfig(func(filename string, config any) error {
bytes, err := os.ReadFile(filepath)
if err != nil {
return err
}
bytes, err := os.ReadFile(filepath)
if err != nil {
return err
}
return json.Unmarshal(bytes, &config)
return json.Unmarshal(bytes, &config)
})
}

View File

@ -1,25 +0,0 @@
{
"Other": {
"0": {
"id": 1,
"name": "张飞"
},
"1": {
"id": 2,
"name": "刘备"
}
},
"Id": 1,
"Count": "a",
"Info": {
"id": 1,
"name": "小明",
"info": {
"lv": 1,
"exp": {
"mux": 10,
"count": 100
}
}
}
}

View File

@ -1,81 +0,0 @@
{
"2": {
"c": {
"Info": {
"id": 1,
"name": "小明",
"info": {
"lv": 1,
"exp": {
"mux": 10,
"count": 100
}
}
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
},
"1": {
"id": 2,
"name": "刘备"
}
},
"Id": 2,
"Count": "c"
},
"d": {
"Id": 2,
"Count": "d",
"Info": {
"id": 1,
"name": "小明",
"info": {
"lv": 1,
"exp": {
"mux": 10,
"count": 100
}
}
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
},
"1": {
"id": 2,
"name": "刘备"
}
}
}
},
"1": {
"b": {
"Id": 1,
"Count": "b",
"Info": {
"id": 1,
"name": "小明",
"info": {
"lv": 1,
"exp": {
"mux": 10,
"count": 100
}
}
},
"Other": {
"0": {
"id": 1,
"name": "张飞"
},
"1": {
"id": 2,
"name": "刘备"
}
}
}
}
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
jsonIter "github.com/json-iterator/go"
"github.com/kercylan98/minotaur/utils/geometry/matrix"
"github.com/kercylan98/minotaur/utils/hash"
"github.com/kercylan98/minotaur/utils/str"
"github.com/kercylan98/minotaur/utils/xlsxtool"
"github.com/tealeg/xlsx"
@ -15,10 +16,14 @@ import (
// NewConfig 定位为空将读取Sheet名称
// - 表格中需要严格遵守 描述、名称、类型、导出参数、数据列的顺序
func NewConfig(sheet *xlsx.Sheet) (*Config, error) {
func NewConfig(sheet *xlsx.Sheet, exist map[string]bool) (*Config, error) {
config := &Config{
ignore: "#",
excludeFields: map[int]bool{0: true},
Exist: exist,
}
if strings.HasPrefix(sheet.Name, config.ignore) {
return nil, ErrReadConfigFailedIgnore
}
if err := config.initField(sheet); err != nil {
return nil, err
@ -27,6 +32,7 @@ func NewConfig(sheet *xlsx.Sheet) (*Config, error) {
}
type Config struct {
Exist map[string]bool
Prefix string
DisplayName string
Name string
@ -68,6 +74,12 @@ func (slf *Config) initField(sheet *xlsx.Sheet) error {
slf.Name = str.FirstUpper(strings.TrimSpace(value.String()))
}
}
if strings.HasPrefix(sheet.Name, slf.ignore) {
return ErrReadConfigFailedIgnore
}
if hash.Exist(slf.Exist, slf.Name) {
return ErrReadConfigFailedSame
}
if indexCount == nil {
slf.IndexCount, _ = strconv.Atoi(sheet.Name)
@ -120,22 +132,22 @@ func (slf *Config) initField(sheet *xlsx.Sheet) error {
describe, fieldName, fieldType, exportParam string
)
var skip bool
if value := slf.matrix.Get(dx, dy); value == nil {
if value, exist := slf.matrix.GetExist(dx, dy); !exist {
skip = true
} else {
describe = value.String()
}
if value := slf.matrix.Get(nx, ny); value == nil {
if value, exist := slf.matrix.GetExist(nx, ny); !exist {
skip = true
} else {
fieldName = str.FirstUpper(strings.TrimSpace(value.String()))
}
if value := slf.matrix.Get(tx, ty); value == nil {
if value, exist := slf.matrix.GetExist(tx, ty); !exist {
skip = true
} else {
fieldType = strings.TrimSpace(value.String())
}
if value := slf.matrix.Get(ex, ey); value == nil {
if value, exist := slf.matrix.GetExist(ex, ey); !exist {
skip = true
} else {
exportParam = strings.ToLower(strings.TrimSpace(value.String()))
@ -233,6 +245,7 @@ func (slf *Config) initData() error {
var lineClient = map[any]any{}
var skip bool
var offset int
var stop bool
for i := 0; i < len(slf.Fields); i++ {
if slf.excludeFields[i] {
if c := slf.matrix.Get(x+i, y); c != nil && strings.HasPrefix(c.String(), "#") {
@ -249,8 +262,8 @@ func (slf *Config) initData() error {
var value any
if slf.horizontal {
c := slf.matrix.Get(x+i, y)
if c == nil || (currentIndex < slf.IndexCount && len(c.String()) == 0) {
skip = true
if c == nil || (currentIndex < slf.IndexCount && len(strings.TrimSpace(c.String())) == 0) {
stop = true
break
}
value = getValueWithType(field.SourceType, c.String())
@ -273,6 +286,9 @@ func (slf *Config) initData() error {
case "sc", "cs":
lineServer[field.Name] = value
lineClient[field.Name] = value
default:
skip = true
break
}
if currentIndex < slf.IndexCount {
@ -304,7 +320,9 @@ func (slf *Config) initData() error {
}
}
}
if slf.horizontal {
if stop {
break
} else if slf.horizontal {
y++
if y >= slf.matrix.GetHeight() {
break
@ -373,7 +391,7 @@ func (slf *Config) GetVariable() string {
}
}
}
return fmt.Sprintf("%s*%s", result, slf.Name)
return fmt.Sprintf("%s*%sDefine", result, slf.Name)
}
func (slf *Config) GetVariableGen() string {

View File

@ -3,6 +3,8 @@ package internal
import "errors"
var (
ErrReadConfigFailedIgnore = errors.New("read config skip ignore")
ErrReadConfigFailedSame = errors.New("read config skip, same name")
ErrReadConfigFailedWithDisplayName = errors.New("read config display name failed, can not found position")
ErrReadConfigFailedWithName = errors.New("read config name failed, can not found position")
ErrReadConfigFailedWithIndexCount = errors.New("read config index count failed, can not found position")

View File

@ -3,12 +3,19 @@ package internal
const (
generateConfigTemplate = `
// {{.Name}} {{.DisplayName}}
type {{.Name}} struct {
// {{.Name}}Define {{.DisplayName}}
type {{.Name}}Define struct {
{{range $index, $value := .Fields}}{{if eq $value.Server true}}{{if eq $value.Ignore false}}{{$value.Name}} {{$value.Type}} // {{$value.Describe}}{{end}}{{end}}
{{end}}
}
func (slf *{{.Name}}Define) String() string {
if data, err := json.Marshal(slf); err == nil {
return string(data)
}
return "{}"
}
{{range $index, $value := .Fields}}{{$value}}{{end}}
`
@ -37,21 +44,22 @@ var json = jsonIter.ConfigCompatibleWithStandardLibrary
var (
{{range $index, $config := .Configs}}
I{{$config.Name}} {{$config.GetVariable}}
i{{$config.Name}} {{$config.GetVariable}}
// {{$config.Name}} {{$config.DisplayName}}
{{$config.Name}} {{$config.GetVariable}}
_{{$config.Name}} {{$config.GetVariable}}
{{end}}
)
func LoadConfig(handle func(filename string, config any) error) {
var err error
{{range $index, $config := .Configs}}
i{{$config.Name}} = {{$config.GetVariableGen}}
_{{$config.Name}} = {{$config.GetVariableGen}}
{{if eq $config.IndexCount 0}}
if err = handle("{{$config.Prefix}}{{$config.Name}}.json", i{{$config.Name}}); err != nil {
if err = handle("{{$config.Prefix}}{{$config.Name}}.json", _{{$config.Name}}); err != nil {
log.Error("Config", zap.String("Name", "{{$config.Name}}"), zap.Bool("Invalid", true), zap.Error(err))
}
{{else}}
if err = handle("{{$config.Prefix}}{{$config.Name}}.json", &i{{$config.Name}}); err != nil {
if err = handle("{{$config.Prefix}}{{$config.Name}}.json", &_{{$config.Name}}); err != nil {
log.Error("Config", zap.String("Name", "{{$config.Name}}"), zap.Bool("Invalid", true), zap.Error(err))
}
{{end}}
@ -60,7 +68,7 @@ func LoadConfig(handle func(filename string, config any) error) {
func Refresh() {
{{range $index, $config := .Configs}}
I{{$config.Name}} = i{{$config.Name}}
{{$config.Name}} = _{{$config.Name}}
{{end}}
}

Binary file not shown.

View File

@ -1,6 +1,7 @@
package matrix
import (
"github.com/kercylan98/minotaur/utils/generic"
"github.com/kercylan98/minotaur/utils/geometry"
)
@ -60,6 +61,19 @@ func (slf *Matrix[T]) Get(x, y int) (value T) {
return slf.m[geometry.CoordinateToPos(slf.w, x, y)]
}
// GetExist 获取特定坐标的内容,如果不存在则返回 false
func (slf *Matrix[T]) GetExist(x, y int) (value T, exist bool) {
pos := geometry.CoordinateToPos(slf.w, x, y)
if pos >= len(slf.m) {
return
}
t := slf.m[pos]
if generic.IsNil(t) {
return
}
return slf.m[pos], true
}
// GetWithPos 获取特定坐标的内容
func (slf *Matrix[T]) GetWithPos(pos int) (value T) {
return slf.m[pos]

View File

@ -1,5 +1,7 @@
package hash
import "encoding/json"
// Exist 检查特定 key 是否存在
func Exist[K comparable, V any](m map[K]V, key K) bool {
_, exist := m[key]
@ -15,3 +17,11 @@ func AllExist[K comparable, V any](m map[K]V, keys ...K) bool {
}
return true
}
// ToJson 将 map 转换为 json 字符串
func ToJson[K comparable, V any](m map[K]V) string {
if data, err := json.Marshal(m); err == nil {
return string(data)
}
return "{}"
}