Files
iknowyou/back/internal/tools/config_reflect.go
2026-04-06 15:12:34 +02:00

156 lines
3.3 KiB
Go

package tools
import (
"reflect"
"strconv"
"strings"
)
// ReflectConfigFields builds []ConfigField from a struct using yaml/iky tags.
// iky tag format: iky:"desc=...;default=...;required=true;options=a|b|c"
func ReflectConfigFields(cfg any) []ConfigField {
v := reflect.ValueOf(cfg)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
var fields []ConfigField
for i := range t.NumField() {
sf := t.Field(i)
fv := v.Field(i)
yamlKey := sf.Tag.Get("yaml")
if yamlKey == "" || yamlKey == "-" {
continue
}
yamlKey = strings.SplitN(yamlKey, ",", 2)[0]
meta := parseIkyTag(sf.Tag.Get("iky"))
fieldType := goKindToString(sf.Type.Kind())
if len(meta.options) > 0 {
fieldType = "enum"
}
fields = append(fields, ConfigField{
Name: yamlKey,
Type: fieldType,
Required: meta.required,
Description: meta.desc,
Default: parseTypedDefault(meta.rawDefault, sf.Type.Kind()),
Value: fv.Interface(),
Options: meta.options,
})
}
return fields
}
// ApplyDefaults sets each field to its iky default if the field is zero.
func ApplyDefaults(cfg any) {
v := reflect.ValueOf(cfg)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
t := v.Type()
for i := range t.NumField() {
sf := t.Field(i)
fv := v.Field(i)
if !fv.CanSet() {
continue
}
meta := parseIkyTag(sf.Tag.Get("iky"))
if meta.rawDefault == "" || !fv.IsZero() {
continue
}
applyDefault(fv, sf.Type.Kind(), meta.rawDefault)
}
}
type ikyMeta struct {
desc string
rawDefault string
required bool
options []string
}
func parseIkyTag(tag string) ikyMeta {
var m ikyMeta
for _, part := range strings.Split(tag, ";") {
k, v, ok := strings.Cut(strings.TrimSpace(part), "=")
if !ok {
continue
}
switch strings.TrimSpace(k) {
case "desc":
m.desc = strings.TrimSpace(v)
case "default":
m.rawDefault = strings.TrimSpace(v)
case "required":
m.required = strings.TrimSpace(v) == "true"
case "options":
for _, opt := range strings.Split(v, "|") {
if o := strings.TrimSpace(opt); o != "" {
m.options = append(m.options, o)
}
}
}
}
return m
}
func goKindToString(k reflect.Kind) string {
switch k {
case reflect.String:
return "string"
case reflect.Bool:
return "bool"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return "int"
case reflect.Float32, reflect.Float64:
return "float"
default:
return k.String()
}
}
func parseTypedDefault(raw string, k reflect.Kind) any {
if raw == "" {
return nil
}
switch k {
case reflect.Bool:
b, _ := strconv.ParseBool(raw)
return b
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n, _ := strconv.ParseInt(raw, 10, 64)
return int(n)
case reflect.Float32, reflect.Float64:
f, _ := strconv.ParseFloat(raw, 64)
return f
default:
return raw
}
}
func applyDefault(fv reflect.Value, k reflect.Kind, raw string) {
switch k {
case reflect.String:
fv.SetString(raw)
case reflect.Bool:
if b, err := strconv.ParseBool(raw); err == nil {
fv.SetBool(b)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if n, err := strconv.ParseInt(raw, 10, 64); err == nil {
fv.SetInt(n)
}
case reflect.Float32, reflect.Float64:
if f, err := strconv.ParseFloat(raw, 64); err == nil {
fv.SetFloat(f)
}
}
}