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) } } }