Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+221
View File
@@ -0,0 +1,221 @@
# Structs
`structs` Provide some extends util functions for struct. eg: convert, tag parse, struct data init
- `structs.Aliases` - implemented a simple string alias map.
- Convert a struct to `map[string]any` data
- Quickly init struct default values by field "default" tag.
- Quickly set struct field values by map data
- Parse a struct and collect tags, and parse tag value
- And more util functions ...
## Install
```shell
go get github.com/gookit/goutil/structs
```
## Go docs
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/structs)
## Usage
### Convert to map
`structs.ToMap()` can be quickly convert a `struct` value to `map[string]any`
**Examples**:
```go
type User1 struct {
Name string `json:"name"`
Age int `json:"age"`
city string
}
u1 := &User1{
Name: "inhere",
Age: 34,
city: "somewhere",
}
mp := structs.ToMap(u1)
dump.P(mp)
```
**Output**:
```shell
map[string]interface {} { #len=2
"age": int(34),
"name": string("inhere"), #len=6
},
```
### Init default values
`structs.InitDefaults` Quickly init struct default value by field "default" tag.
**Examples**:
```go
type Extra struct {
City string `default:"chengdu"`
Github string `default:"https://github.com/inhere"`
}
type User struct {
Name string `default:"inhere"`
Age int `default:"30"`
Extra Extra `default:""` // NEED mark tag
}
u := &User{}
_ = structs.InitDefaults(u, &structs.InitOptions{})
dump.P(u)
```
**Output**:
```shell
&structs_test.User {
Name: string("inhere"), #len=6
Age: int(30),
Extra: structs_test.Extra {
City: string("chengdu"), #len=7
Github: string("https://github.com/inhere"), #len=25
},
},
```
### Set values from map
```go
data := map[string]interface{}{
"name": "inhere",
"age": 234,
"tags": []string{"php", "go"},
"city": "chengdu",
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
Tags []string `json:"tags"`
City string `json:"city"`
}
u := &User{}
err := structs.SetValues(u, data)
dump.P(u)
```
**Output**:
```shell
&structs_test.User {
Name: string("inhere"), #len=6
Age: int(234),
Tags: []string [ #len=2
string("php"), #len=3
string("go"), #len=2
],
City: string("chengdu"), #len=7
},
```
### Tags collect and parse
Parse a struct for collect tags, and parse tag value
**Examples**:
```go
// eg: "desc;required;default;shorts"
type MyCmd struct {
Name string `flag:"set your name;false;INHERE;n"`
}
c := &MyCmd{}
p := structs.NewTagParser("flag")
sepStr := ";"
defines := []string{"desc", "required", "default", "shorts"}
p.ValueFunc = structs.ParseTagValueDefine(sepStr, defines)
goutil.MustOK(p.Parse(c))
dump.P(p.Tags())
```
Output:
```shell
map[string]maputil.SMap { #len=1
"Name": maputil.SMap { #len=1
"flag": string("set your name;false;INHERE;n"), #len=28
},
},
```
**Parse tag value**
```go
info, _ := p.Info("Name", "flag")
dump.P(info)
```
Output:
```shell
maputil.SMap { #len=4
"desc": string("set your name"), #len=13
"required": string("false"), #len=5
"default": string("INHERE"), #len=6
"shorts": string("n"), #len=1
},
```
## Functions API
```go
func InitDefaults(ptr any, optFns ...InitOptFunc) error
func MustToMap(st any, optFns ...MapOptFunc) map[string]interface{}
func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SMap, error)
func ParseTagValueDefault(field, tagVal string) (mp maputil.SMap, err error)
func ParseTagValueNamed(field, tagVal string, keys ...string) (mp maputil.SMap, err error)
func ParseTags(st any, tagNames []string) (map[string]maputil.SMap, error)
func SetValues(ptr any, data map[string]any, optFns ...SetOptFunc) error
func StructToMap(st any, optFns ...MapOptFunc) (map[string]interface{}, error)
func ToMap(st any, optFns ...MapOptFunc) map[string]interface{}
func TryToMap(st any, optFns ...MapOptFunc) (map[string]interface{}, error)
type Aliases struct{ ... }
func NewAliases(checker func(alias string)) *Aliases
type Data struct{ ... }
func NewData() *Data
type InitOptFunc func(opt *InitOptions)
type InitOptions struct{ ... }
type LiteData struct{ ... }
type MapOptFunc func(opt *MapOptions)
type MapOptions struct{ ... }
type SMap struct{ ... }
type SetOptFunc func(opt *SetOptions)
type SetOptions struct{ ... }
type TagParser struct{ ... }
func NewTagParser(tagNames ...string) *TagParser
type TagValFunc func(field, tagVal string) (maputil.SMap, error)
func ParseTagValueDefine(sep string, defines []string) TagValFunc
type Value struct{ ... }
func NewValue(val any) *Value
```
## Testings
```shell
go test -v ./structs/...
```
Test limit by regexp:
```shell
go test -v -run ^TestSetByKeys ./structs/...
```
+61
View File
@@ -0,0 +1,61 @@
# Structs
`structs` 包为 go struct 提供一些有用的工具函数库。例如:convert、tag parse、struct data init
- 快速将结构体转换为 `map[string]any` 数据
- `structs.Aliases` - 实现了一个简单的字符串别名映射。
- 通过字段 `"default"` 标签快速初始化结构体默认值
- 通过map数据快速设置 struct 字段值
- 解析 struct 并收集 tag,解析 tag 值
- 以及更多 util 函数...
## Install
```shell
go get github.com/gookit/goutil/structs
```
## Go docs
- [Go docs](https://pkg.go.dev/github.com/gookit/goutil/structs)
## 初始化结构体
- 支持初始化使用环境变量
- 支持初始化 slice 字段,嵌套结构体
```go
type ExtraDefault struct {
City string `default:"some where"`
Github string `default:"${ GITHUB_ADDR }"`
}
type User struct {
Name string `default:"inhere"`
Age int `default:"300"`
Extra *ExtraDefault `default:""` // 标记需要初始化
}
optFn := func(opt *structs.InitOptions) {
opt.ParseEnv = true
}
obj := &User{}
err := structs.InitDefaults(obj, optFn)
goutil.PanicErr(err)
dump.P(obj)
```
**初始化结果**:
```go
&structs_test.User {
Name: string("inhere"), #len=6
Age: int(300),
Extra: &structs_test.ExtraDefault {
City: string("some where"), #len=10
Github: string("https://some .... url"), #len=21
},
},
```
+66
View File
@@ -0,0 +1,66 @@
package structs
import "fmt"
// Aliases implemented a simple string alias map.
type Aliases struct {
mapping map[string]string
// Checker custom add alias name checker func
Checker func(alias string) // should return bool OR error ??
}
// NewAliases create
func NewAliases(checker func(alias string)) *Aliases {
return &Aliases{Checker: checker}
}
// AddAlias to the Aliases
func (as *Aliases) AddAlias(real, alias string) {
if as.mapping == nil {
as.mapping = make(map[string]string)
}
if as.Checker != nil {
as.Checker(alias)
}
if rn, ok := as.mapping[alias]; ok {
panic(fmt.Sprintf("The alias '%s' is already used by '%s'", alias, rn))
}
as.mapping[alias] = real
}
// AddAliases to the Aliases
func (as *Aliases) AddAliases(real string, aliases []string) {
for _, a := range aliases {
as.AddAlias(real, a)
}
}
// AddAliasMap to the Aliases
func (as *Aliases) AddAliasMap(alias2real map[string]string) {
for a, r := range alias2real {
as.AddAlias(r, a)
}
}
// HasAlias in the Aliases
func (as *Aliases) HasAlias(alias string) bool {
if _, ok := as.mapping[alias]; ok {
return true
}
return false
}
// ResolveAlias by given name. if not exists, return the alias self
func (as *Aliases) ResolveAlias(alias string) string {
if name, ok := as.mapping[alias]; ok {
return name
}
return alias
}
// Mapping get all aliases mapping
func (as *Aliases) Mapping() map[string]string {
return as.mapping
}
+221
View File
@@ -0,0 +1,221 @@
package structs
import (
"errors"
"fmt"
"reflect"
"time"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/reflects"
)
// ToMap quickly convert structs to map by reflection
func ToMap(st any, optFns ...MapOptFunc) map[string]any {
mp, _ := StructToMap(st, optFns...)
return mp
}
// MustToMap alis of TryToMap, but will panic on error
func MustToMap(st any, optFns ...MapOptFunc) map[string]any {
mp, err := StructToMap(st, optFns...)
if err != nil {
panic(err)
}
return mp
}
// TryToMap simple convert structs to map by reflect
func TryToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
return StructToMap(st, optFns...)
}
// ToSMap quickly and safe convert structs to map[string]string by reflection
func ToSMap(st any, optFns ...MapOptFunc) map[string]string {
mp, _ := StructToMap(st, optFns...)
return maputil.ToStringMap(mp)
}
// TryToSMap quickly convert structs to map[string]string by reflection
func TryToSMap(st any, optFns ...MapOptFunc) (map[string]string, error) {
mp, err := StructToMap(st, optFns...)
if err != nil {
return nil, err
}
return maputil.ToStringMap(mp), nil
}
// MustToSMap alias of ToStringMap(), but will panic on error
func MustToSMap(st any, optFns ...MapOptFunc) map[string]string {
mp, err := StructToMap(st, optFns...)
if err != nil {
panic(err)
}
return maputil.ToStringMap(mp)
}
// ToString quickly format struct to string
func ToString(st any, optFns ...MapOptFunc) string {
mp, err := StructToMap(st, optFns...)
if err == nil {
return maputil.ToString(mp)
}
return fmt.Sprint(st)
}
const defaultFieldTag = "json"
// CustomUserFunc for map convert
// - fName: raw field name in struct
//
// Returns:
// - ok: return true to collect field, otherwise excluded.
// - newVal: `newVal != nil` return new value to collect, otherwise collect original value.
type CustomUserFunc func(fName string, fv reflect.Value) (ok bool, newVal any)
// MapOptions for convert struct to map
type MapOptions struct {
// TagName for map filed. default is "json"
TagName string
// ParseDepth for parse and collect. TODO support depth
ParseDepth int
// MergeAnonymous struct fields to parent map. default is true
MergeAnonymous bool
// ExportPrivate export private fields. default is false
ExportPrivate bool
// IgnoreEmpty ignore empty value item. default: false
IgnoreEmpty bool
// UserFunc custom interceptor for filter or handle field value.
UserFunc CustomUserFunc
}
// MapOptFunc define
type MapOptFunc func(opt *MapOptions)
// WithMapTagName set tag name for map field
func WithMapTagName(tagName string) MapOptFunc {
return func(opt *MapOptions) { opt.TagName = tagName }
}
// WithUserFunc custom user func
func WithUserFunc(fn CustomUserFunc) MapOptFunc {
return func(opt *MapOptions) { opt.UserFunc = fn }
}
// MergeAnonymous merge anonymous struct fields to parent map
func MergeAnonymous(opt *MapOptions) { opt.MergeAnonymous = true }
// ExportPrivate merge anonymous struct fields to parent map
func ExportPrivate(opt *MapOptions) { opt.ExportPrivate = true }
// WithIgnoreEmpty ignore on field value is empty
func WithIgnoreEmpty(opt *MapOptions) { opt.IgnoreEmpty = true }
// StructToMap quickly convert structs to map[string]any by reflection.
//
// Can custom export field name by tag `json` or custom tag. see MapOptions
func StructToMap(st any, optFns ...MapOptFunc) (map[string]any, error) {
mp := make(map[string]any)
if st == nil {
return mp, nil
}
obj := reflect.Indirect(reflect.ValueOf(st))
if obj.Kind() != reflect.Struct {
return mp, errors.New("StructToMap: must be an struct value")
}
opt := &MapOptions{TagName: defaultFieldTag}
for _, fn := range optFns {
fn(opt)
}
_, err := structToMap(obj, opt, mp)
return mp, err
}
func structToMap(obj reflect.Value, opt *MapOptions, mp map[string]any) (map[string]any, error) {
if mp == nil {
mp = make(map[string]any)
}
refType := obj.Type()
for i := 0; i < obj.NumField(); i++ {
sf := refType.Field(i)
name := sf.Name
// skip un-exported field
if !opt.ExportPrivate && IsUnexported(name) {
continue
}
tagVal, ok := sf.Tag.Lookup(opt.TagName)
if ok && tagVal != "" {
sMap, err := ParseTagValueDefault(name, tagVal)
if err != nil {
return nil, err
}
name = sMap.Default("name", name)
if name == "" || name == "-" { // un-exported field
continue
}
}
fv := reflect.Indirect(obj.Field(i))
if !fv.IsValid() {
continue
}
// opt: ignore empty field
if opt.IgnoreEmpty && reflects.IsEmpty(fv) {
continue
}
if fv.Kind() == reflect.Struct {
// up: special handle time.Time field value
if reflects.IsTimeType(fv.Type()) {
mp[name] = fv.Interface().(time.Time).Format(time.RFC3339)
continue
}
// collect anonymous struct values to parent.
if sf.Anonymous && opt.MergeAnonymous {
_, err := structToMap(fv, opt, mp)
if err != nil {
return nil, err
}
} else { // collect struct values to submap
sub, err := structToMap(fv, opt, nil)
if err != nil {
return nil, err
}
mp[name] = sub
}
continue
}
// TODO support struct slice field.
// up: support custom user func
if opt.UserFunc != nil {
ok1, newVal := opt.UserFunc(sf.Name, fv)
if !ok1 {
continue
}
// ok1=true, newVal != nil
if newVal != nil {
mp[name] = newVal
continue
}
}
if fv.CanInterface() {
mp[name] = fv.Interface()
} else if fv.CanAddr() { // for unexported field
mp[name] = reflects.UnexportedValue(fv)
}
}
return mp, nil
}
+6
View File
@@ -0,0 +1,6 @@
package structs
// MapStruct simple copy src struct value to dst struct
// func MapStruct(srcSt, dstSt any) {
// // TODO
// }
+174
View File
@@ -0,0 +1,174 @@
package structs
import (
"sync"
"github.com/gookit/goutil/internal/comfunc"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/mathutil"
"github.com/gookit/goutil/strutil"
)
// LiteData simple map[string]any struct. no lock
type LiteData = Data
// NewLiteData create, not locked
func NewLiteData(data map[string]any) *Data {
if data == nil {
data = make(map[string]any)
}
return &LiteData{data: data}
}
/*************************************************************
* data struct and allow enable lock
*************************************************************/
// Data struct, allow enable lock
type Data struct {
sync.RWMutex
lock bool
data map[string]any
}
// NewData create new data instance
func NewData() *Data {
return &Data{
lock: true,
data: make(map[string]any),
}
}
// WithLock for operate data
func (d *Data) WithLock() *Data {
d.lock = true
return d
}
// EnableLock for operate data
func (d *Data) EnableLock() *Data {
return d.WithLock()
}
// Data get all
func (d *Data) Data() map[string]any {
return d.data
}
// SetData set all data
func (d *Data) SetData(data map[string]any) {
if !d.lock {
d.data = data
return
}
d.RLock()
d.data = data
d.RUnlock()
}
// DataLen of data
func (d *Data) DataLen() int {
return len(d.data)
}
// ResetData all data
func (d *Data) ResetData() {
d.data = make(map[string]any)
}
// Merge load new data
func (d *Data) Merge(mp map[string]any) {
d.data = maputil.SimpleMerge(mp, d.data)
}
// Set value to data
func (d *Data) Set(key string, val any) {
d.SetValue(key, val)
}
// SetValue to data
func (d *Data) SetValue(key string, val any) {
if d.lock {
d.Lock()
defer d.Unlock()
}
d.data[key] = val
}
// Value get from data
func (d *Data) Value(key string) (val any, ok bool) {
if d.lock {
d.RLock()
defer d.RUnlock()
}
val, ok = maputil.GetByPath(key, d.data)
return
}
// Get val from data
func (d *Data) Get(key string) any {
return d.GetVal(key)
}
// GetVal get from data
func (d *Data) GetVal(key string) any {
if d.lock {
d.RLock()
defer d.RUnlock()
}
val, _ := maputil.GetByPath(key, d.data)
return val
}
// StrVal get from data
func (d *Data) StrVal(key string) string {
return strutil.QuietString(d.GetVal(key))
}
// IntVal get from data
func (d *Data) IntVal(key string) int {
return mathutil.QuietInt(d.GetVal(key))
}
// BoolVal get from data
func (d *Data) BoolVal(key string) bool {
val, ok := d.Value(key)
if !ok {
return false
}
return comfunc.Bool(val)
}
// String format data
func (d *Data) String() string {
return maputil.ToString(d.data)
}
// OrderedData data TODO
type OrderedData struct {
maputil.Data
cap int
keys []string
// vals []any
}
// NewOrderedData instance.
func NewOrderedData(cap int) *OrderedData {
return &OrderedData{cap: cap, Data: make(maputil.Data, cap)}
}
// Load data
func (om *OrderedData) Load(data map[string]any) {
om.Data.Load(data)
om.keys = om.Data.Keys()
}
// Set key and value to map
func (om *OrderedData) Set(key string, val any) {
om.keys = append(om.keys, key)
om.Data.Set(key, val)
}
+276
View File
@@ -0,0 +1,276 @@
package structs
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/gookit/goutil/internal/varexpr"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/strutil"
)
const defaultInitTag = "default"
const defaultEnvPrefixTag = "defaultenvprefix"
// InitOptFunc define
type InitOptFunc func(opt *InitOptions)
// InitOptions struct
type InitOptions struct {
// TagName default value tag name. tag: default
TagName string
// EnvPrefix default ENV prefix name.
EnvPrefix string
// EnvPrefixTagName default value tag name. tag: defaultenvprefix
EnvPrefixTagName string
// ParseEnv var name on default value. eg: `default:"${APP_ENV}"`
//
// default: false
ParseEnv bool
// ParseTime parse string to `time.Duration`, `time.Time`. default: false
//
// eg: default:"10s", default:"2025-04-23 15:04:05"
ParseTime bool
// ValueHook before set value hook TODO
ValueHook func(val string) any
}
// WithParseTime set parse time string on default value.
func (opt *InitOptions) WithParseTime(val bool) *InitOptions {
opt.ParseTime = val
return opt
}
// WithParseEnv set parse env var on default value.
func (opt *InitOptions) WithParseEnv(val bool) *InitOptions {
opt.ParseEnv = val
return opt
}
// Init struct default value by field "default" tag.
func Init(ptr any, optFns ...InitOptFunc) error {
return InitDefaults(ptr, optFns...)
}
// InitDefaults init struct default value by field "default" tag.
//
// TIPS:
//
// Support init field types: string, bool, intX, uintX, floatX, array, slice
//
// Example:
//
// type User1 struct {
// Name string `default:"inhere"`
// Age int32 `default:"30"`
// }
//
// u1 := &User1{}
// err = structs.InitDefaults(u1)
// fmt.Printf("%+v\n", u1) // Output: {Name:inhere Age:30}
func InitDefaults(ptr any, optFns ...InitOptFunc) error {
rv := reflect.ValueOf(ptr)
if !reflects.IsValidPtr(rv) {
return errors.New("must be provider an pointer value")
}
rv = rv.Elem()
if rv.Kind() != reflect.Struct {
return errors.New("must be provider an struct value")
}
opt := &InitOptions{TagName: defaultInitTag, EnvPrefixTagName: defaultEnvPrefixTag}
for _, fn := range optFns {
fn(opt)
}
return initDefaults(rv, opt, "")
}
// type InitBuilder struct {
// opt InitOptions
// }
func initDefaults(rv reflect.Value, opt *InitOptions, envPrefix string) error {
rt := rv.Type()
opt.EnvPrefix = envPrefix
for i := 0; i < rt.NumField(); i++ {
sf := rt.Field(i)
// skip doesn't exported field
if IsUnexported(sf.Name) {
continue
}
val, hasTag := sf.Tag.Lookup(opt.TagName)
if !hasTag || val == "-" {
continue
}
var childPrefixVar string
prefixVar, _ := sf.Tag.Lookup(opt.EnvPrefixTagName)
if prefixVar != "" {
childPrefixVar = fmt.Sprintf("%s%s", envPrefix, prefixVar)
// opt.EnvPrefix = childPrefixVar
}
fv := rv.Field(i)
if fv.Kind() == reflect.Struct {
// special: struct is time.Time type
if reflects.IsTimeType(fv.Type()) {
if err := initDefaultValue(fv, val, opt); err != nil {
return err
}
} else if err := initDefaults(fv, opt, childPrefixVar); err != nil {
return err
}
continue
}
// Skip init on field has value. but will check slice and pointer field
if !fv.IsZero() {
// special: handle for pointer struct field
if fv.Kind() == reflect.Pointer {
fv = fv.Elem()
if fv.Kind() == reflect.Struct {
if err := initDefaults(fv, opt, childPrefixVar); err != nil {
return err
}
}
} else if fv.Kind() == reflect.Slice {
el := sf.Type.Elem()
if el.Kind() == reflect.Pointer {
el = el.Elem()
}
// init sub struct in slice. like `[]SubStruct` or `[]*SubStruct`
if el.Kind() == reflect.Struct && fv.Len() > 0 {
for i := 0; i < fv.Len(); i++ {
subFv := reflect.Indirect(fv.Index(i))
if err := initDefaults(subFv, opt, childPrefixVar); err != nil {
return err
}
}
}
}
continue
}
// handle for pointer field
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
fv.Set(reflect.New(fv.Type().Elem()))
}
fv = fv.Elem()
if fv.Kind() == reflect.Struct {
if err := initDefaults(fv, opt, childPrefixVar); err != nil {
return err
}
continue
}
} else if fv.Kind() == reflect.Slice {
el := sf.Type.Elem()
isPtr := el.Kind() == reflect.Pointer
if isPtr {
el = el.Elem()
}
// init sub struct in slice. like `[]SubStruct` or `[]*SubStruct`
if el.Kind() == reflect.Struct {
// up: if slice elem is struct and slice len=0, will be skip init default value
if fv.Len() == 0 {
continue
}
// make sub-struct and init. like: `SubStruct`
subFv := reflect.New(el)
subFvE := subFv.Elem()
if err := initDefaults(subFvE, opt, childPrefixVar); err != nil {
return err
}
// make new slice and set value.
newFv := reflect.MakeSlice(reflect.SliceOf(sf.Type.Elem()), 0, 1)
if isPtr {
newFv = reflect.Append(newFv, subFv)
} else {
newFv = reflect.Append(newFv, subFvE)
}
fv.Set(newFv)
continue
}
}
if err := initDefaultValue(fv, val, opt); err != nil {
return err
}
}
return nil
}
func enhanceDefaultVar(val string, envPrefix string) string {
if len(envPrefix) == 0 {
return val
}
cleanedVar := strings.TrimSpace(val)
if strings.HasPrefix(cleanedVar, "${") && strings.HasSuffix(cleanedVar, "}") {
parts := strings.SplitN(cleanedVar[2:len(cleanedVar)-1], "|", 2)
if len(parts) > 0 {
env := strings.TrimSpace(parts[0])
if env != "" {
if len(parts) == 1 {
return fmt.Sprintf("${%s%s}", envPrefix, env)
} else {
return fmt.Sprintf("${%s%s|%s}", envPrefix, env, parts[1])
}
}
}
}
return val
}
func initDefaultValue(fv reflect.Value, val string, opt *InitOptions) error {
if val == "" || !fv.CanSet() {
return nil
}
// parse env var
if opt.ParseEnv {
val = varexpr.SafeParse(enhanceDefaultVar(val, opt.EnvPrefix))
}
var anyVal any = val
// enhance: parse special value type. eg: time.Duration, time.Time
if opt.ParseTime {
newVal, err := reflects.ToTimeOrDuration(val, fv.Type())
if err != nil {
return err
}
anyVal = newVal
}
// simple slice: convert simple kind(string,intX,uintX,...) to slice. eg: "1,2,3" => []int{1,2,3}
if reflects.IsArrayOrSlice(fv.Kind()) && reflects.IsSimpleKind(reflects.SliceElemKind(fv.Type())) {
ss := strutil.SplitTrimmed(val, ",")
valRv, err := reflects.ConvSlice(reflect.ValueOf(ss), fv.Type().Elem())
if err == nil {
if fv.Kind() == reflect.Array {
for i := 0; i < valRv.Len(); i++ {
fv.Index(i).Set(valRv.Index(i))
}
} else {
reflects.SetRValue(fv, valRv)
}
}
return err
}
// set value
return reflects.SetValue(fv, anyVal)
}
+6
View File
@@ -0,0 +1,6 @@
package structs
// SMap simple map[string]string struct. TODO
type SMap struct {
data map[string]string //lint:ignore U1000 for unused
}
+15
View File
@@ -0,0 +1,15 @@
// Package structs Provide some extends util functions for struct. eg: tag parse, struct init, value set
package structs
// IsExported field name on struct
func IsExported(name string) bool {
return name[0] >= 'A' && name[0] <= 'Z'
}
// IsUnexported field name on struct
func IsUnexported(name string) bool {
if name[0] == '_' {
return true
}
return name[0] >= 'a' && name[0] <= 'z'
}
+273
View File
@@ -0,0 +1,273 @@
package structs
import (
"errors"
"fmt"
"reflect"
"strings"
"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/strutil"
)
// ErrNotAnStruct error
// var emptyStringMap = make(maputil.SMap)
var ErrNotAnStruct = errors.New("must input an struct value")
// ParseTags for parse struct tags.
func ParseTags(st any, tagNames []string) (map[string]maputil.SMap, error) {
p := NewTagParser(tagNames...)
if err := p.Parse(st); err != nil {
return nil, err
}
return p.Tags(), nil
}
// ParseReflectTags parse struct tags info.
func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SMap, error) {
p := NewTagParser(tagNames...)
if err := p.ParseType(rt); err != nil {
return nil, err
}
return p.Tags(), nil
}
// TagValFunc handle func
type TagValFunc func(field, tagVal string) (maputil.SMap, error)
// TagParser struct
type TagParser struct {
// TagNames want parsed tag names.
TagNames []string
// ValueFunc tag value parse func.
ValueFunc TagValFunc
// key: field name
// value: tag map {tag-name: value string.}
tags map[string]maputil.SMap
}
// Tags map data for struct fields
func (p *TagParser) Tags() map[string]maputil.SMap {
return p.tags
}
// NewTagParser instance
func NewTagParser(tagNames ...string) *TagParser {
return &TagParser{
TagNames: tagNames,
ValueFunc: ParseTagValueDefault,
}
}
// Parse an struct value
func (p *TagParser) Parse(st any) error {
rv := reflect.ValueOf(st)
if rv.Kind() == reflect.Ptr && !rv.IsNil() {
rv = rv.Elem()
}
return p.ParseType(rv.Type())
}
// ParseType parse a struct type value
func (p *TagParser) ParseType(rt reflect.Type) error {
if rt.Kind() != reflect.Struct {
return ErrNotAnStruct
}
// key is field name.
p.tags = make(map[string]maputil.SMap)
return p.parseType(rt, "")
}
func (p *TagParser) parseType(rt reflect.Type, parent string) error {
for i := 0; i < rt.NumField(); i++ {
sf := rt.Field(i)
// skip don't exported field
name := sf.Name
if name[0] >= 'a' && name[0] <= 'z' {
continue
}
smp := make(maputil.SMap)
for _, tagName := range p.TagNames {
// eg: `json:"age"`
// eg: "name=int0;shorts=i;required=true;desc=int option message"
tagVal := sf.Tag.Get(tagName)
if tagVal == "" {
continue
}
smp[tagName] = tagVal
}
pathKey := name
if parent != "" {
pathKey = parent + "." + name
}
p.tags[pathKey] = smp
ft := sf.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
// field is struct.
if ft.Kind() == reflect.Struct {
err := p.parseType(ft, pathKey)
if err != nil {
return err
}
}
}
return nil
}
// Info parse the give field, returns tag value info.
//
// info, err := p.Info("Name", "json")
// exportField := info.Get("name")
func (p *TagParser) Info(field, tag string) (maputil.SMap, error) {
field = strutil.UpperFirst(field)
fTags, ok := p.tags[field]
if !ok {
return nil, fmt.Errorf("field %q not found", field)
}
val, ok := fTags.Value(tag)
if !ok {
return make(maputil.SMap), nil
}
// parse tag value
return p.ValueFunc(field, val)
}
/*************************************************************
* some built in tag value parse func
*************************************************************/
// ParseTagValueDefault parse like json tag value.
//
// see json.Marshal():
//
// // JSON as key "myName", skipped if empty.
// Field int `json:"myName,omitempty"`
//
// // Field appears in JSON as key "Field" (the default), but skipped if empty.
// Field int `json:",omitempty"`
//
// // Field is ignored by this package.
// Field int `json:"-"`
//
// // Field appears in JSON as key "-".
// Field int `json:"-,"`
//
// Int64String int64 `json:",string"`
//
// Returns:
//
// {
// "name": "myName", // maybe is empty, on tag value is "-"
// "omitempty": "true",
// "string": "true",
// // ... more custom bool settings.
// }
func ParseTagValueDefault(field, tagVal string) (mp maputil.SMap, err error) {
ss := strutil.SplitTrimmed(tagVal, ",")
ln := len(ss)
if ln == 0 || tagVal == "," {
return maputil.SMap{"name": field}, nil
}
mp = make(maputil.SMap, ln)
if ln == 1 {
// valid field name
if ss[0] != "-" {
mp["name"] = ss[0]
}
return
}
// ln > 1
mp["name"] = ss[0]
// other settings: omitempty, string
for _, key := range ss[1:] {
mp[key] = "true"
}
return
}
// ParseTagValueQuick quick parse tag value string by sep(;)
func ParseTagValueQuick(tagVal string, defines []string) maputil.SMap {
parseFn := ParseTagValueDefine(";", defines)
mp, _ := parseFn("", tagVal)
return mp
}
// ParseTagValueDefine parse tag value string by given defines.
//
// Examples:
//
// eg: "desc;required;default;shorts"
// type MyStruct {
// Age int `flag:"int option message;;a,b"`
// }
// sepStr := ";"
// defines := []string{"desc", "required", "default", "shorts"}
func ParseTagValueDefine(sep string, defines []string) TagValFunc {
defNum := len(defines)
return func(field, tagVal string) (maputil.SMap, error) {
ss := strutil.SplitNTrimmed(tagVal, sep, defNum)
ln := len(ss)
mp := make(maputil.SMap, ln)
if ln == 0 {
return mp, nil
}
for i, val := range ss {
key := defines[i]
mp[key] = val
}
return mp, nil
}
}
// ParseTagValueNamed parse k-v tag value string. it's like INI format contents.
//
// Examples:
//
// eg: "name=val0;shorts=i;required=true;desc=a message"
// =>
// {name: val0, shorts: i, required: true, desc: a message}
func ParseTagValueNamed(field, tagVal string, keys ...string) (mp maputil.SMap, err error) {
ss := strutil.Split(tagVal, ";")
ln := len(ss)
if ln == 0 {
return
}
mp = make(maputil.SMap, ln)
for _, s := range ss {
if !strings.ContainsRune(s, '=') {
err = fmt.Errorf("parse tag error on field '%s': must match `KEY=VAL`", field)
return
}
key, val := strutil.TrimCut(s, "=")
if len(keys) > 0 && !arrutil.StringsHas(keys, key) {
err = fmt.Errorf("parse tag error on field '%s': invalid key name '%s'", field, key)
return
}
mp[key] = val
}
return
}
+147
View File
@@ -0,0 +1,147 @@
package structs
import (
"github.com/gookit/goutil/arrutil"
"github.com/gookit/goutil/comdef"
"github.com/gookit/goutil/mathutil"
"github.com/gookit/goutil/strutil"
)
// Value data store
type Value struct {
// V value
V any
}
// NewValue instance.
func NewValue(val any) *Value {
return &Value{
V: val,
}
}
// Set value
func (v *Value) Set(val any) {
v.V = val
}
// Reset value
func (v *Value) Reset() {
v.V = nil
}
// Val get
func (v *Value) Val() any {
return v.V
}
// Val get
// func (v *Value) ValOr[T any](defVal T) T {
// return v.V
// }
// Int value get
func (v *Value) Int() int {
if v.V == nil {
return 0
}
return mathutil.QuietInt(v.V)
}
// Int64 value
func (v *Value) Int64() int64 {
if v.V == nil {
return 0
}
return mathutil.QuietInt64(v.V)
}
// Bool value
func (v *Value) Bool() bool {
if v.V == nil {
return false
}
if bl, ok := v.V.(bool); ok {
return bl
}
if str, ok := v.V.(string); ok {
return strutil.QuietBool(str)
}
return false
}
// Float64 value
func (v *Value) Float64() float64 {
if v.V == nil {
return 0
}
return mathutil.QuietFloat(v.V)
}
// String value
func (v *Value) String() string {
if v.V == nil {
return ""
}
if str, ok := v.V.(string); ok {
return str
}
return strutil.QuietString(v.V)
}
// Strings value
func (v *Value) Strings() (ss []string) {
if v.V == nil {
return
}
if ss, ok := v.V.([]string); ok {
return ss
}
if str, ok := v.V.(string); ok {
return strutil.Split(str, comdef.DefaultSep)
}
return
}
// SplitToStrings split string value to strings. sep default is comma(,)
func (v *Value) SplitToStrings(sep ...string) (ss []string) {
if v.V == nil {
return
}
if str, ok := v.V.(string); ok {
return strutil.Split(str, sepStr(sep))
}
return
}
// SplitToInts split string value to []int. sep default is comma(,)
func (v *Value) SplitToInts(sep ...string) (ss []int) {
if v.V == nil {
return
}
if str, ok := v.V.(string); ok {
ints, err := arrutil.StringsToInts(strutil.Split(str, sepStr(sep)))
if err == nil {
return ints
}
}
return
}
// IsEmpty value
func (v *Value) IsEmpty() bool {
return v.V == nil
}
func sepStr(seps []string) string {
if len(seps) > 0 {
return seps[0]
}
return comdef.DefaultSep
}
+75
View File
@@ -0,0 +1,75 @@
package structs
import (
"errors"
"reflect"
)
// Wrapper struct for read or set field value
type Wrapper struct {
// src any // source struct
// raw reflect.Value of source struct
rv reflect.Value
// FieldTagName field name for read/write value. default tag: json
FieldTagName string
// caches for field rv and name and tag name TODO
fieldNames []string //lint:ignore U1000 for unused
fvCacheMap map[string]reflect.Value //lint:ignore U1000 for unused
}
// Wrap quick create a struct wrapper
func Wrap(src any) *Wrapper { return NewWrapper(src) }
// NewWrapper create a struct wrapper
func NewWrapper(src any) *Wrapper {
return WrapValue(reflect.ValueOf(src))
}
// WrapValue create a struct wrapper
func WrapValue(rv reflect.Value) *Wrapper {
rv = reflect.Indirect(rv)
if rv.Kind() != reflect.Struct {
panic("must be provider an struct value")
}
return &Wrapper{rv: rv}
}
// Get field value by name, name allows to use dot syntax.
func (r *Wrapper) Get(name string) any {
val, ok := r.Lookup(name)
if !ok {
return nil
}
return val
}
// Lookup field value by name, name allows to use dot syntax.
func (r *Wrapper) Lookup(name string) (val any, ok bool) {
fv := r.rv.FieldByName(name)
if !fv.IsValid() {
return
}
if fv.CanInterface() {
return fv.Interface(), true
}
return
}
// Set field value by name. name allows using dot syntax.
func (r *Wrapper) Set(name string, val any) error {
fv := r.rv.FieldByName(name)
if !fv.IsValid() {
return errors.New("field " + name + " not found")
}
if !fv.CanSet() {
return errors.New("can not set value for field: " + name)
}
fv.Set(reflect.ValueOf(val))
return nil
}
+228
View File
@@ -0,0 +1,228 @@
package structs
import (
"errors"
"fmt"
"reflect"
"github.com/gookit/goutil/comdef"
"github.com/gookit/goutil/reflects"
"github.com/gookit/goutil/strutil"
)
// NewWriter create a struct writer
//
// TIP: must be pointer for set field value
func NewWriter(ptr any) *Wrapper {
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Pointer {
panic("must be provider an pointer value")
}
return WrapValue(rv)
}
/*************************************************************
* set values to a struct
*************************************************************/
// SetOptFunc define
type SetOptFunc func(opt *SetOptions)
// BeforeSetFunc hook type.
type BeforeSetFunc func(fieldName string, value any, fv reflect.Value) any
// SetOptions for set values to struct
type SetOptions struct {
// FieldTagName get field name for read value. default tag: json
FieldTagName string
// BeforeSetFn hook func. will fire on before set value.
// - you can modify value here.
// - returns nil will skip set value.
// - returns value will be set to field value.
BeforeSetFn BeforeSetFunc
// ParseTime parse string to `time.Duration`, `time.Time`. default: false
//
// eg: default:"10s", default:"2025-04-23 15:04:05"
ParseTime bool
// ParseDefault init default value by DefaultValTag tag value. default: false
//
// see InitDefaults()
ParseDefault bool
// DefaultValTag name. tag: default
DefaultValTag string
// ParseDefaultEnv parse env var on default tag. eg: `default:"${APP_ENV}"` default: false
ParseDefaultEnv bool
// DefaultEnvPrefixTag name. tag: defaultenvprefix
DefaultEnvPrefixTag string
// StopOnError if true, will stop set value on error happened. default: false
// StopOnError bool
}
// WithParseDefault value by tag "default"
func WithParseDefault(opt *SetOptions) { opt.ParseDefault = true }
// WithBeforeSetFn value by tag "default"
func WithBeforeSetFn(fn BeforeSetFunc) SetOptFunc {
return func(opt *SetOptions) { opt.BeforeSetFn = fn }
}
// TODO refactoring SetValues to the struct
type ValuesSetter struct {
src any // source struct
// raw reflect.Value of source struct
rv reflect.Value
option *SetOptions
initOpt *InitOptions
}
// BindData set values to struct ptr from map data.
func BindData(ptr any, data map[string]any, optFns ...SetOptFunc) error {
return SetValues(ptr, data, optFns...)
}
// SetValues set values to struct ptr from map data.
//
// TIPS:
//
// Only support set: string, bool, intX, uintX, floatX
func SetValues(ptr any, data map[string]any, optFns ...SetOptFunc) error {
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Ptr {
return errors.New("must be provider an pointer value")
}
rv = rv.Elem()
if rv.Kind() != reflect.Struct {
return errors.New("must be provider an struct value")
}
opt := &SetOptions{
FieldTagName: defaultFieldTag,
DefaultValTag: defaultInitTag,
DefaultEnvPrefixTag: defaultEnvPrefixTag,
}
for _, fn := range optFns {
fn(opt)
}
return setValues(rv, data, opt, "")
}
func setValues(rv reflect.Value, data map[string]any, opt *SetOptions, envPrefix string) error {
if len(data) == 0 {
return nil
}
var es comdef.Errors
initOpt := &InitOptions{
EnvPrefix: envPrefix,
ParseEnv: opt.ParseDefaultEnv,
ParseTime: opt.ParseTime,
}
rt := rv.Type()
for i := 0; i < rt.NumField(); i++ {
ft := rt.Field(i)
name := ft.Name
// skip don't exported field
if name[0] >= 'a' && name[0] <= 'z' {
continue
}
// get field name
tagVal, ok0 := ft.Tag.Lookup(opt.FieldTagName)
if ok0 {
info, err := ParseTagValueDefault(name, tagVal)
if err != nil {
es = append(es, err)
continue
}
name = info.Get("name")
}
fv := rv.Field(i)
val, ok := data[name]
// set field value by default tag.
if !ok && opt.ParseDefault && fv.IsZero() {
defVal := ft.Tag.Get(opt.DefaultValTag)
if err := initDefaultValue(fv, defVal, initOpt); err != nil {
es = append(es, err)
}
continue
}
// handle for pointer field
if fv.Kind() == reflect.Pointer {
if fv.IsNil() {
fv.Set(reflect.New(fv.Type().Elem()))
}
fv = fv.Elem()
}
// hook: call BeforeSet func
if opt.BeforeSetFn != nil {
val = opt.BeforeSetFn(ft.Name, val, fv)
if val == nil {
continue
}
ok = true // on value is not nil
}
// field is struct
if fv.Kind() == reflect.Struct {
valRf := reflects.Indirect(reflect.ValueOf(val))
// up: special handle time.Time field
if reflects.IsTimeType(fv.Type()) {
// maybe val is time.Time
if reflects.IsTimeType(valRf.Type()) {
if err := reflects.SetValue(fv, valRf); err != nil {
es = append(es, err)
}
continue
}
// val is datetime string
tm, err := strutil.ToTime(strutil.StringOr(val, ""))
if err != nil {
es = append(es, err)
} else if err = reflects.SetValue(fv, tm); err != nil {
es = append(es, err)
}
continue
}
// val as map
asMp, err := reflects.TryAnyMap(valRf)
if err != nil {
err = fmt.Errorf("must provide map for set struct field %q, err=%v", ft.Name, err)
es = append(es, err)
continue
}
defEnvPrefixVal := ft.Tag.Get(opt.DefaultEnvPrefixTag)
childEnvPrefix := fmt.Sprintf("%s%s", envPrefix, defEnvPrefixVal)
// recursive processing sub-struct
if err = setValues(fv, asMp, opt, childEnvPrefix); err != nil {
es = append(es, err)
}
continue
}
// set field value
if ok && val != nil {
if err := reflects.SetValue(fv, val); err != nil {
es = append(es, err)
}
}
}
return es.ErrOrNil()
}