Files
monibuca/pkg/config/config.go

435 lines
9.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package config
import (
"encoding/json"
"fmt"
"log/slog"
"maps"
"os"
"reflect"
"regexp"
"strings"
"time"
"github.com/mcuadros/go-defaults"
"gopkg.in/yaml.v3"
)
type Config struct {
Ptr reflect.Value //指向配置结构体值,优先级:动态修改值>环境变量>配置文件>defaultYaml>全局配置>默认值
Modify any //动态修改的值
Env any //环境变量中的值
File any //配置文件中的值
Global *Config //全局配置中的值,指针类型
Default any //默认值
Enum []struct {
Label string `json:"label"`
Value any `json:"value"`
}
name string // 小写
propsMap map[string]*Config
props []*Config
tag reflect.StructTag
}
var (
durationType = reflect.TypeOf(time.Duration(0))
regexpType = reflect.TypeOf(Regexp{})
basicTypes = []reflect.Kind{
reflect.Bool,
reflect.Int,
reflect.Int8,
reflect.Int16,
reflect.Int32,
reflect.Int64,
reflect.Uint,
reflect.Uint8,
reflect.Uint16,
reflect.Uint32,
reflect.Uint64,
reflect.Float32,
reflect.Float64,
reflect.String,
}
)
func (config *Config) Range(f func(key string, value Config)) {
if m, ok := config.GetValue().(map[string]Config); ok {
for k, v := range m {
f(k, v)
}
}
}
func (config *Config) IsMap() bool {
_, ok := config.GetValue().(map[string]Config)
return ok
}
func (config *Config) Get(key string) (v *Config) {
if config.propsMap == nil {
config.propsMap = make(map[string]*Config)
}
key = strings.ToLower(key)
if v, ok := config.propsMap[key]; ok {
return v
} else {
v = &Config{
name: key,
}
config.propsMap[key] = v
config.props = append(config.props, v)
return v
}
}
func (config *Config) Has(key string) (ok bool) {
if config.propsMap == nil {
return false
}
_, ok = config.propsMap[strings.ToLower(key)]
return ok
}
func (config *Config) MarshalJSON() ([]byte, error) {
if config.propsMap == nil {
return json.Marshal(config.GetValue())
}
return json.Marshal(config.propsMap)
}
func (config *Config) GetValue() any {
return config.Ptr.Interface()
}
// Parse 第一步读取配置结构体的默认值
func (config *Config) Parse(s any, prefix ...string) {
var t reflect.Type
var v reflect.Value
if vv, ok := s.(reflect.Value); ok {
t, v = vv.Type(), vv
} else {
t, v = reflect.TypeOf(s), reflect.ValueOf(s)
}
if t.Kind() == reflect.Pointer {
t, v = t.Elem(), v.Elem()
}
isStruct := t.Kind() == reflect.Struct && t != regexpType
if isStruct {
defaults.SetDefaults(v.Addr().Interface())
}
config.Ptr = v
if !v.IsValid() {
fmt.Println("parse to ", prefix, config.name, s, "is not valid")
return
}
if l := len(prefix); l > 0 { // 读取环境变量
name := strings.ToLower(prefix[l-1])
_, isUnmarshaler := v.Addr().Interface().(yaml.Unmarshaler)
tag := config.tag.Get("default")
if tag != "" && isUnmarshaler {
v.Set(config.assign(name, tag))
}
if envValue := os.Getenv(strings.Join(prefix, "_")); envValue != "" {
v.Set(config.assign(name, envValue))
config.Env = v.Interface()
}
}
config.Default = v.Interface()
if isStruct {
for i, j := 0, t.NumField(); i < j; i++ {
ft, fv := t.Field(i), v.Field(i)
if !ft.IsExported() {
continue
}
name := strings.ToLower(ft.Name)
if name == "plugin" {
continue
}
if tag := ft.Tag.Get("yaml"); tag != "" {
if tag == "-" {
continue
}
name, _, _ = strings.Cut(tag, ",")
}
prop := config.Get(name)
prop.tag = ft.Tag
if len(prefix) > 0 {
prop.Parse(fv, append(prefix, strings.ToUpper(ft.Name))...)
} else {
prop.Parse(fv)
}
for _, kv := range strings.Split(ft.Tag.Get("enum"), ",") {
kvs := strings.Split(kv, ":")
if len(kvs) != 2 {
continue
}
var tmp struct {
Value any
}
yaml.Unmarshal([]byte(fmt.Sprintf("value: %s", strings.TrimSpace(kvs[0]))), &tmp)
prop.Enum = append(prop.Enum, struct {
Label string `json:"label"`
Value any `json:"value"`
}{
Label: strings.TrimSpace(kvs[1]),
Value: tmp.Value,
})
}
}
}
}
// ParseDefaultYaml 第二步读取全局配置
func (config *Config) ParseGlobal(g *Config) {
config.Global = g
if config.propsMap != nil {
for k, v := range config.propsMap {
v.ParseGlobal(g.Get(k))
}
} else {
config.Ptr.Set(g.Ptr)
}
}
// ParseDefaultYaml 第三步读取内嵌默认配置
func (config *Config) ParseDefaultYaml(defaultYaml map[string]any) {
if defaultYaml == nil {
return
}
for k, v := range defaultYaml {
if config.Has(k) {
if prop := config.Get(k); prop.props != nil {
if v != nil {
prop.ParseDefaultYaml(v.(map[string]any))
}
} else {
dv := prop.assign(k, v)
prop.Default = dv.Interface()
if prop.Env == nil {
prop.Ptr.Set(dv)
}
}
}
}
}
// ParseFile 第四步读取用户配置文件
func (config *Config) ParseUserFile(conf map[string]any) {
if conf == nil {
return
}
config.File = conf
for k, v := range conf {
k = strings.ReplaceAll(k, "-", "")
k = strings.ReplaceAll(k, "_", "")
k = strings.ToLower(k)
if config.Has(k) {
if prop := config.Get(k); prop.props != nil {
if v != nil {
switch vv := v.(type) {
case map[string]any:
prop.ParseUserFile(vv)
default:
prop.props[0].Ptr.Set(reflect.ValueOf(v))
}
}
} else {
fv := prop.assign(k, v)
if fv.IsValid() {
prop.File = fv.Interface()
if prop.Env == nil {
prop.Ptr.Set(fv)
}
} else {
// continue invalid field
slog.Error("Attempted to access invalid field during config parsing: %s", v)
}
}
}
}
}
// ParseModifyFile 第五步读取动态修改配置文件
func (config *Config) ParseModifyFile(conf map[string]any) {
if conf == nil {
return
}
config.Modify = conf
for k, v := range conf {
if config.Has(k) {
if prop := config.Get(k); prop.props != nil {
if v != nil {
vmap := v.(map[string]any)
prop.ParseModifyFile(vmap)
if len(vmap) == 0 {
delete(conf, k)
}
}
} else {
mv := prop.assign(k, v)
v = mv.Interface()
vwm := prop.valueWithoutModify()
if equal(vwm, v) {
delete(conf, k)
if prop.Modify != nil {
prop.Modify = nil
prop.Ptr.Set(reflect.ValueOf(vwm))
}
continue
}
prop.Modify = v
prop.Ptr.Set(mv)
}
}
}
if len(conf) == 0 {
config.Modify = nil
}
}
func (config *Config) valueWithoutModify() any {
if config.Env != nil {
return config.Env
}
if config.File != nil {
return config.File
}
if config.Global != nil {
return config.Global.GetValue()
}
return config.Default
}
func equal(vwm, v any) bool {
switch ft := reflect.TypeOf(vwm); ft {
case regexpType:
return vwm.(Regexp).String() == v.(Regexp).String()
default:
switch ft.Kind() {
case reflect.Slice, reflect.Array, reflect.Map:
return reflect.DeepEqual(vwm, v)
}
return vwm == v
}
}
func (config *Config) GetMap() map[string]any {
m := make(map[string]any)
for k, v := range config.propsMap {
if v.props != nil {
if vv := v.GetMap(); vv != nil {
m[k] = vv
}
} else if v.GetValue() != nil {
m[k] = v.GetValue()
}
}
if len(m) > 0 {
return m
}
return nil
}
var regexPureNumber = regexp.MustCompile(`^\d+$`)
func unmarshal(ft reflect.Type, v any) (target reflect.Value) {
source := reflect.ValueOf(v)
for _, t := range basicTypes {
if source.Kind() == t && ft.Kind() == t {
return source
}
}
switch ft {
case durationType:
target = reflect.New(ft).Elem()
if source.Type() == durationType {
return source
} else if source.IsZero() || !source.IsValid() {
target.SetInt(0)
} else {
timeStr := source.String()
if d, err := time.ParseDuration(timeStr); err == nil && !regexPureNumber.MatchString(timeStr) {
target.SetInt(int64(d))
} else {
slog.Error("invalid duration value please add unit (s,m,h,d)eg: 100ms, 10s, 4m, 1h", "value", timeStr)
os.Exit(1)
}
}
case regexpType:
target = reflect.New(ft).Elem()
regexpStr := source.String()
target.Set(reflect.ValueOf(Regexp{regexp.MustCompile(regexpStr)}))
default:
switch ft.Kind() {
case reflect.Struct:
newStruct := reflect.New(ft)
defaults.SetDefaults(newStruct.Interface())
if value, ok := v.(map[string]any); ok {
for i := 0; i < ft.NumField(); i++ {
key := strings.ToLower(ft.Field(i).Name)
if vv, ok := value[key]; ok {
newStruct.Elem().Field(i).Set(unmarshal(ft.Field(i).Type, vv))
}
}
} else {
newStruct.Elem().Field(0).Set(unmarshal(ft.Field(0).Type, v))
}
return newStruct.Elem()
case reflect.Map:
if v != nil {
target = reflect.MakeMap(ft)
for k, v := range v.(map[string]any) {
target.SetMapIndex(unmarshal(ft.Key(), k), unmarshal(ft.Elem(), v))
}
}
case reflect.Slice:
if v != nil {
s := v.([]any)
target = reflect.MakeSlice(ft, len(s), len(s))
for i, v := range s {
target.Index(i).Set(unmarshal(ft.Elem(), v))
}
}
default:
if v != nil {
var out []byte
var err error
if vv, ok := v.(string); ok {
out = []byte(fmt.Sprintf("%s: %s", "value", vv))
} else {
out, err = yaml.Marshal(map[string]any{"value": v})
if err != nil {
panic(err)
}
}
tmpValue := reflect.New(reflect.StructOf([]reflect.StructField{
{
Name: "Value",
Type: ft,
},
}))
err = yaml.Unmarshal(out, tmpValue.Interface())
if err != nil {
panic(err)
}
return tmpValue.Elem().Field(0)
}
}
}
return
}
func (config *Config) assign(k string, v any) reflect.Value {
return unmarshal(config.Ptr.Type(), v)
}
func Parse(target any, conf map[string]any) {
var c Config
c.Parse(target)
c.ParseModifyFile(maps.Clone(conf))
}