package sysconf import ( "encoding" "encoding/csv" "errors" "fmt" "os" "reflect" "sort" "strconv" "strings" "time" ) type Config struct { ini *Ini envPrefix string envLookup func(string) (string, bool) } type ConfigFieldInfo struct { Section string Key string Field string Type string Default string Env string Split string Required bool } type ConfigEntryInfo struct { Section string Key string Path string Values []string NoValue bool } type ConfigOption func(*Config) type ConfigSource interface { Load(*Config) error } type ConfigValidator interface { Validate() error } type ConfigError struct { Section string Key string Field string Reason string Err error } type ConfigSourceError struct { Path string Optional bool Err error } type FileSource struct { Path string Optional bool } type MemorySource struct { Name string Data []byte } func (e *ConfigError) Error() string { if e == nil { return "" } location := e.Key if e.Section != "" { location = e.Section + "." + e.Key } if e.Field != "" { return fmt.Sprintf("sysconf config %s (%s): %s", location, e.Field, e.Reason) } return fmt.Sprintf("sysconf config %s: %s", location, e.Reason) } func (e *ConfigError) Unwrap() error { if e == nil { return nil } return e.Err } func (e *ConfigSourceError) Error() string { if e == nil { return "" } kind := "required" if e.Optional { kind = "optional" } return fmt.Sprintf("sysconf config %s source %q: %v", kind, e.Path, e.Err) } func (e *ConfigSourceError) Unwrap() error { if e == nil { return nil } return e.Err } func (s FileSource) Load(cfg *Config) error { if cfg == nil { return ErrDocumentClosed } if s.Path == "" { return nil } if err := cfg.LoadFile(s.Path); err != nil { if s.Optional && os.IsNotExist(err) { return nil } return &ConfigSourceError{Path: s.Path, Optional: s.Optional, Err: err} } return nil } func (s MemorySource) Load(cfg *Config) error { if cfg == nil { return ErrDocumentClosed } if err := cfg.LoadBytes(s.Data); err != nil { return &ConfigSourceError{Path: s.Name, Err: err} } return nil } func RequiredFile(path string) FileSource { return FileSource{Path: path} } func OptionalFile(path string) FileSource { return FileSource{Path: path, Optional: true} } func BytesSource(name string, data []byte) MemorySource { return MemorySource{Name: name, Data: append([]byte(nil), data...)} } func StringSource(name, data string) MemorySource { return BytesSource(name, []byte(data)) } func DescribeConfig(src interface{}) ([]ConfigFieldInfo, error) { v, err := configReadableStructValue(src) if err != nil { return nil, err } fields := make([]ConfigFieldInfo, 0) err = walkReadableConfigFields(v, "", "", func(field configField) error { fields = append(fields, ConfigFieldInfo{ Section: field.section, Key: field.key, Field: field.fieldPath, Type: field.structField.Type.String(), Default: field.defaultValue, Env: field.env, Split: field.split, Required: field.required, }) return nil }) if err != nil { return nil, err } return fields, nil } func SampleConfig(src interface{}) ([]byte, error) { v, err := configReadableStructValue(src) if err != nil { return nil, err } cfg := NewConfig() err = walkReadableConfigFields(v, "", "", func(field configField) error { values, err := sampleValuesForField(field) if err != nil { return configFieldErrorWithErr(field, "invalid sample value: "+err.Error(), err) } cfg.setAll(field.section, field.key, values, "") return nil }) if err != nil { return nil, err } return cfg.Build(), nil } func NewConfig(options ...ConfigOption) *Config { cfg := &Config{ ini: NewIni(), } for _, option := range options { if option != nil { option(cfg) } } if cfg.ini == nil { cfg.ini = NewIni() } return cfg } func LoadConfig(dst interface{}, files []string, options ...ConfigOption) (*Config, error) { sources := make([]ConfigSource, 0, len(files)) for _, path := range files { sources = append(sources, RequiredFile(path)) } return LoadConfigSources(dst, sources, options...) } func LoadConfigSources(dst interface{}, sources []ConfigSource, options ...ConfigOption) (*Config, error) { cfg := NewConfig(options...) if err := cfg.LoadSources(sources...); err != nil { return nil, err } if dst != nil { if err := cfg.Bind(dst); err != nil { return nil, err } } return cfg, nil } func WithEnvPrefix(prefix string) ConfigOption { return func(cfg *Config) { cfg.envPrefix = strings.Trim(prefix, "_") if cfg.envLookup == nil { cfg.envLookup = os.LookupEnv } } } func WithEnvLookup(lookup func(string) (string, bool)) ConfigOption { return func(cfg *Config) { cfg.envLookup = lookup } } func WithIni(ini *Ini) ConfigOption { return func(cfg *Config) { cfg.ini = ini } } func (c *Config) Ini() *Ini { if c == nil { return nil } if c.ini == nil { c.ini = NewIni() } return c.ini } func (c *Config) Section(name string) *Section { if c == nil { return nil } return c.Ini().Section(name) } func (c *Config) Get(section, key string) string { if c == nil { return "" } return c.Ini().Get(section, key) } func (c *Config) GetAll(section, key string) []string { if c == nil { return nil } return c.Ini().GetAll(section, key) } func (c *Config) SectionNames() []string { if c == nil { return nil } ini := c.Ini() if ini == nil || ini.Document == nil { return nil } ini.Document.mu.RLock() defer ini.Document.mu.RUnlock() seen := make(map[string]string) for _, section := range ini.Document.sections { if section == nil { continue } if section.Name == "" && !sectionHasPairs(section) { continue } normalized := normalize(section.Name, ini.CaseSensitive) if _, ok := seen[normalized]; !ok { seen[normalized] = section.Name } } names := make([]string, 0, len(seen)) for _, name := range seen { names = append(names, name) } sort.Strings(names) return names } func (c *Config) Keys(section string) []string { if c == nil { return nil } ini := c.Ini() if ini == nil || ini.Document == nil { return nil } ini.Document.mu.RLock() defer ini.Document.mu.RUnlock() wanted := normalize(section, ini.CaseSensitive) seen := make(map[string]string) for _, sec := range ini.Document.sections { if sec == nil || normalize(sec.Name, ini.CaseSensitive) != wanted { continue } for _, entry := range sec.Entries { if !isConfigEntry(entry) { continue } normalized := normalize(entry.Key, sec.CaseSensitive) if _, ok := seen[normalized]; !ok { seen[normalized] = entry.Key } } } keys := make([]string, 0, len(seen)) for _, key := range seen { keys = append(keys, key) } sort.Strings(keys) return keys } func (c *Config) Flatten() map[string][]string { out := make(map[string][]string) for _, entry := range c.FlattenEntries() { if entry.NoValue || len(entry.Values) == 0 { out[entry.Path] = append(out[entry.Path], "") continue } out[entry.Path] = append(out[entry.Path], entry.Values...) } return out } func (c *Config) FlattenEntries() []ConfigEntryInfo { if c == nil { return nil } ini := c.Ini() if ini == nil || ini.Document == nil { return nil } ini.Document.mu.RLock() defer ini.Document.mu.RUnlock() entries := make([]ConfigEntryInfo, 0) for _, section := range ini.Document.sections { if section == nil { continue } for _, entry := range section.Entries { if !isConfigEntry(entry) { continue } item := ConfigEntryInfo{ Section: section.Name, Key: entry.Key, Path: configPath(section.Name, entry.Key), Values: append([]string(nil), entry.Values...), NoValue: entry.NoValue || len(entry.Values) == 0, } entries = append(entries, item) } } return entries } func (c *Config) GetStringE(section, key string) (string, error) { item, err := c.firstValue(section, key) if err != nil { return "", err } return item.text, nil } func (c *Config) GetBoolE(section, key string) (bool, error) { item, err := c.firstValue(section, key) if err != nil { return false, err } if item.noValue { return true, nil } value, err := strconv.ParseBool(item.text) if err != nil { return false, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) GetIntE(section, key string) (int, error) { item, err := c.firstValue(section, key) if err != nil { return 0, err } value, err := strconv.Atoi(item.text) if err != nil { return 0, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) GetInt64E(section, key string) (int64, error) { item, err := c.firstValue(section, key) if err != nil { return 0, err } value, err := strconv.ParseInt(item.text, 10, 64) if err != nil { return 0, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) GetUint64E(section, key string) (uint64, error) { item, err := c.firstValue(section, key) if err != nil { return 0, err } value, err := strconv.ParseUint(item.text, 10, 64) if err != nil { return 0, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) GetFloat64E(section, key string) (float64, error) { item, err := c.firstValue(section, key) if err != nil { return 0, err } value, err := strconv.ParseFloat(item.text, 64) if err != nil { return 0, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) GetDurationE(section, key string) (time.Duration, error) { item, err := c.firstValue(section, key) if err != nil { return 0, err } value, err := time.ParseDuration(item.text) if err != nil { return 0, configKeyError(section, key, err.Error(), err) } return value, nil } func (c *Config) Has(section, key string) bool { if c == nil { return false } return c.Ini().Has(section, key) } func (c *Config) Set(section, key, value string) { if c == nil { return } c.Ini().Set(section, key, value) } func (c *Config) SetAll(section, key string, values []string) { if c == nil { return } c.setAll(section, key, values, "") } func (c *Config) Delete(section, key string) bool { if c == nil { return false } return c.Ini().Delete(section, key) } func (c *Config) DeleteSection(section string) bool { if c == nil { return false } return c.Ini().DeleteSection(section) } func (c *Config) Build() []byte { if c == nil { return nil } return c.Ini().Build() } func (c *Config) Bytes() []byte { return c.Build() } func (c *Config) Save(path string) error { if c == nil { return ErrDocumentClosed } return c.Ini().Save(path) } func (c *Config) SaveAtomic(path string) error { if c == nil { return ErrDocumentClosed } return c.Ini().SaveAtomic(path) } func (c *Config) LoadBytes(data []byte) error { if c == nil { return ErrDocumentClosed } tmp := newIniLike(c.Ini()) if err := tmp.Parse(data); err != nil { return err } c.merge(tmp) return nil } func newIniLike(base *Ini) *Ini { tmp := NewIni() if base == nil || base.Document == nil { return tmp } tmp.SectionOpen = base.SectionOpen tmp.SectionClose = base.SectionClose tmp.Assign = base.Assign tmp.AssignDelimiters = append([]string(nil), base.AssignDelimiters...) tmp.CommentHeads = append([]string(nil), base.CommentHeads...) tmp.AllowInline = base.AllowInline tmp.InlineCommentRequiresSpace = base.InlineCommentRequiresSpace tmp.AllowNoValue = base.AllowNoValue tmp.AllowMulti = base.AllowMulti tmp.AllowContinuation = base.AllowContinuation tmp.TrimSpace = base.TrimSpace tmp.CaseSensitive = base.CaseSensitive tmp.Strict = base.Strict return tmp } func (c *Config) LoadFile(path string) error { data, err := os.ReadFile(path) if err != nil { return err } return c.LoadBytes(data) } func (c *Config) LoadSource(source ConfigSource) error { if c == nil { return ErrDocumentClosed } if source == nil { return nil } return source.Load(c) } func (c *Config) LoadSources(sources ...ConfigSource) error { for _, source := range sources { if source == nil { continue } if err := c.LoadSource(source); err != nil { return err } } return nil } func (c *Config) LoadFiles(paths ...string) error { sources := make([]ConfigSource, 0, len(paths)) for _, path := range paths { sources = append(sources, RequiredFile(path)) } return c.LoadSources(sources...) } func (c *Config) LoadOptionalFiles(paths ...string) error { sources := make([]ConfigSource, 0, len(paths)) for _, path := range paths { sources = append(sources, OptionalFile(path)) } return c.LoadSources(sources...) } func (c *Config) SetDefault(section, key, value string) { c.SetDefaultAll(section, key, []string{value}) } func (c *Config) SetDefaultAll(section, key string, values []string) { if c == nil || key == "" { return } ini := c.Ini() if ini.Has(section, key) { return } ini.Document.mu.Lock() sec := ini.Document.ensureSection(section) ini.Document.mu.Unlock() if sec != nil { _ = sec.SetAll(key, append([]string(nil), values...), "") } } func (c *Config) Bind(dst interface{}) error { if c == nil { return ErrDocumentClosed } v, err := configStructValue(dst) if err != nil { return err } if err := c.applyStructDefaults(v, ""); err != nil { return err } if err := c.applyEnv(v, ""); err != nil { return err } if err := c.checkRequired(v, ""); err != nil { return err } if err := c.bindStruct(v, ""); err != nil { return err } return validateConfig(dst) } func (c *Config) SetStruct(src interface{}) error { if c == nil { return ErrDocumentClosed } v, err := configReadableStructValue(src) if err != nil { return err } return c.setStruct(v, "") } type mergeValue struct { key string values []string comment string noValue bool } func (c *Config) merge(src *Ini) { if c == nil || src == nil || src.Document == nil { return } sections := make(map[string]map[string]*mergeValue) sectionOrder := make([]string, 0) keyOrder := make(map[string][]string) for _, section := range src.Document.Sections() { if section == nil { continue } secKey := normalize(section.Name, src.CaseSensitive) if _, ok := sections[secKey]; !ok { sections[secKey] = make(map[string]*mergeValue) sectionOrder = append(sectionOrder, section.Name) } for _, entry := range section.Entries { if entry == nil || entry.kind != linePair { continue } entryKey := normalize(entry.Key, section.CaseSensitive) mv := sections[secKey][entryKey] if mv == nil { mv = &mergeValue{} sections[secKey][entryKey] = mv keyOrder[secKey] = append(keyOrder[secKey], entry.Key) } mv.key = entry.Key mv.comment = entry.Comment mv.noValue = entry.NoValue if entry.NoValue { mv.values = nil } else { mv.values = append(mv.values, entry.Values...) } } } for _, sectionName := range sectionOrder { secKey := normalize(sectionName, src.CaseSensitive) for _, key := range keyOrder[secKey] { mv := sections[secKey][normalize(key, src.CaseSensitive)] if mv == nil { continue } c.setAll(sectionName, mv.key, mv.values, mv.comment) } } } func (c *Config) setAll(section, key string, values []string, comment string) { ini := c.Ini() ini.Document.mu.Lock() sec := ini.Document.ensureSection(section) ini.Document.mu.Unlock() if sec != nil { _ = sec.SetAll(key, append([]string(nil), values...), comment) } } func configStructValue(dst interface{}) (reflect.Value, error) { if dst == nil { return reflect.Value{}, errors.New("destination is nil") } v := reflect.ValueOf(dst) if v.Kind() != reflect.Ptr || v.IsNil() { return reflect.Value{}, errors.New("destination must be a non-nil pointer") } v = v.Elem() if v.Kind() != reflect.Struct { return reflect.Value{}, errors.New("destination must point to a struct") } return v, nil } func configReadableStructValue(src interface{}) (reflect.Value, error) { if src == nil { return reflect.Value{}, errors.New("source is nil") } v := reflect.ValueOf(src) for v.Kind() == reflect.Ptr { if v.IsNil() { return reflect.Value{}, errors.New("source must be a non-nil pointer or struct") } v = v.Elem() } if v.Kind() != reflect.Struct { return reflect.Value{}, errors.New("source must be struct") } return v, nil } func (c *Config) applyStructDefaults(v reflect.Value, inheritedSection string) error { return c.walkConfigFields(v, inheritedSection, func(field configField) error { if field.defaultValue == "" || c.has(field.section, field.key) { return nil } values, err := defaultValuesForField(field, field.defaultValue) if err != nil { return configFieldErrorWithErr(field, "invalid default: "+err.Error(), err) } c.setAll(field.section, field.key, values, "") return nil }) } func (c *Config) applyEnv(v reflect.Value, inheritedSection string) error { if c.envLookup == nil { return nil } return c.walkConfigFields(v, inheritedSection, func(field configField) error { envName := c.envName(field) if envName == "" { return nil } value, ok := c.envLookup(envName) if !ok { return nil } values, err := defaultValuesForField(field, value) if err != nil { return configFieldErrorWithErr(field, "invalid env "+envName+": "+err.Error(), err) } c.setAll(field.section, field.key, values, "") return nil }) } func (c *Config) checkRequired(v reflect.Value, inheritedSection string) error { return c.walkConfigFields(v, inheritedSection, func(field configField) error { if !field.required { return nil } items := c.values(field.section, field.key) if len(items) == 0 { return configFieldError(field, "required value is missing") } for _, item := range items { if item.noValue { if configFieldAcceptsNoValue(field.value) { return nil } continue } if strings.TrimSpace(item.text) != "" { return nil } } return configFieldError(field, "required value is empty") }) } func (c *Config) bindStruct(v reflect.Value, inheritedSection string) error { return c.walkConfigFields(v, inheritedSection, func(field configField) error { items, err := expandConfigValues(field.value, c.values(field.section, field.key), field.split) if err != nil { return configFieldErrorWithErr(field, err.Error(), err) } if len(items) == 0 { return nil } if err := setConfigValueItems(field.value, items); err != nil { return configFieldErrorWithErr(field, err.Error(), err) } return nil }) } func (c *Config) setStruct(v reflect.Value, inheritedSection string) error { t := v.Type() for idx := 0; idx < t.NumField(); idx++ { field := t.Field(idx) value := v.Field(idx) if !value.CanInterface() { continue } section := field.Tag.Get("seg") if section == "" { section = inheritedSection } key := field.Tag.Get("key") if nested, ok := nestedConfigValueForWrite(value, key); ok { if nested.IsValid() { if err := c.setStruct(nested, section); err != nil { return err } } continue } if key == "" { key = fieldNameKey(field.Name) } if key == "-" { continue } cfgField := configField{ value: value, structField: field, fieldPath: field.Name, section: section, key: key, } values, err := outputValuesForField(cfgField) if err != nil { return configFieldErrorWithErr(cfgField, err.Error(), err) } if values == nil { continue } c.setAll(section, key, values, "") } return nil } type configField struct { value reflect.Value structField reflect.StructField fieldPath string section string key string env string defaultValue string split string required bool } func (c *Config) walkConfigFields(v reflect.Value, inheritedSection string, visit func(configField) error) error { t := v.Type() for idx := 0; idx < t.NumField(); idx++ { field := t.Field(idx) value := v.Field(idx) if !value.CanSet() { continue } section := field.Tag.Get("seg") if section == "" { section = inheritedSection } key := field.Tag.Get("key") if isNestedConfigStruct(value, key) { nested := value for nested.Kind() == reflect.Ptr { if nested.IsNil() { nested.Set(reflect.New(nested.Type().Elem())) } nested = nested.Elem() } if err := c.walkConfigFields(nested, section, visit); err != nil { return err } continue } if key == "" { key = fieldNameKey(field.Name) } if key == "-" { continue } cfgField := configField{ value: value, structField: field, fieldPath: field.Name, section: section, key: key, env: field.Tag.Get("env"), defaultValue: field.Tag.Get("default"), split: field.Tag.Get("split"), required: parseTagBool(field.Tag.Get("required")), } if err := visit(cfgField); err != nil { return err } } return nil } func walkReadableConfigFields(v reflect.Value, inheritedSection, inheritedPath string, visit func(configField) error) error { t := v.Type() for idx := 0; idx < t.NumField(); idx++ { field := t.Field(idx) if field.PkgPath != "" { continue } value := v.Field(idx) section := field.Tag.Get("seg") if section == "" { section = inheritedSection } fieldPath := joinFieldPath(inheritedPath, field.Name) key := field.Tag.Get("key") if isNestedConfigFieldType(field.Type, key) { nested := readableNestedValue(value, field.Type) if err := walkReadableConfigFields(nested, section, fieldPath, visit); err != nil { return err } continue } if key == "" { key = fieldNameKey(field.Name) } if key == "-" { continue } cfgField := configField{ value: value, structField: field, fieldPath: fieldPath, section: section, key: key, env: field.Tag.Get("env"), defaultValue: field.Tag.Get("default"), split: field.Tag.Get("split"), required: parseTagBool(field.Tag.Get("required")), } if err := visit(cfgField); err != nil { return err } } return nil } func isNestedConfigStruct(value reflect.Value, key string) bool { if key != "" { return false } t := value.Type() for t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() != reflect.Struct { return false } if t.PkgPath() == "time" { return false } return !implementsTextUnmarshaler(t) } func isNestedConfigFieldType(t reflect.Type, key string) bool { if key != "" { return false } for t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() != reflect.Struct { return false } if t.PkgPath() == "time" { return false } return !implementsTextUnmarshaler(t) } func readableNestedValue(value reflect.Value, t reflect.Type) reflect.Value { for t.Kind() == reflect.Ptr { t = t.Elem() } for value.Kind() == reflect.Ptr { if value.IsNil() { return reflect.Zero(t) } value = value.Elem() } return value } func nestedConfigValueForWrite(value reflect.Value, key string) (reflect.Value, bool) { if !isNestedConfigStruct(value, key) { return reflect.Value{}, false } for value.Kind() == reflect.Ptr { if value.IsNil() { return reflect.Value{}, true } value = value.Elem() } return value, true } func configFieldAcceptsNoValue(value reflect.Value) bool { t := value.Type() for t.Kind() == reflect.Ptr { t = t.Elem() } return t.Kind() == reflect.Bool } func (c *Config) has(section, key string) bool { return c.Ini().Has(section, key) } func (c *Config) value(section, key string) string { return c.Ini().Get(section, key) } type configValue struct { text string noValue bool } func (c *Config) values(section, key string) []configValue { if c == nil { return nil } return configValuesFromSections(c.Ini().Sections(section), key) } func (c *Config) firstValue(section, key string) (configValue, error) { items := c.values(section, key) if len(items) == 0 { return configValue{}, configKeyError(section, key, "value is missing", ErrKeyNotFound) } return items[0], nil } func (c *Config) envName(field configField) string { if field.env == "-" { return "" } if field.env != "" { return field.env } parts := make([]string, 0, 3) if c.envPrefix != "" { parts = append(parts, c.envPrefix) } if field.section != "" { parts = append(parts, field.section) } parts = append(parts, field.key) return normalizeEnvName(strings.Join(parts, "_")) } func normalizeEnvName(name string) string { var builder strings.Builder builder.Grow(len(name)) underscore := false for _, r := range name { if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') { if r >= 'a' && r <= 'z' { r -= 'a' - 'A' } builder.WriteRune(r) underscore = false continue } if !underscore { builder.WriteByte('_') underscore = true } } return strings.Trim(builder.String(), "_") } func joinFieldPath(prefix, name string) string { if prefix == "" { return name } return prefix + "." + name } func fieldNameKey(name string) string { var builder strings.Builder for idx, r := range name { if r >= 'A' && r <= 'Z' { if idx > 0 { builder.WriteByte('_') } r += 'a' - 'A' } builder.WriteRune(r) } return builder.String() } func parseTagBool(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "t", "true", "y", "yes", "required": return true default: return false } } func sampleValuesForField(field configField) ([]string, error) { if field.defaultValue != "" { return defaultValuesForField(field, field.defaultValue) } values, err := outputValuesForField(field) if err != nil { return nil, err } if field.required && needsRequiredSamplePlaceholder(values) { return requiredSampleValuesForType(field.value.Type()) } if len(values) > 0 { return values, nil } values, err = sampleValuesForType(field.value.Type()) if err != nil { return nil, err } if field.required && needsRequiredSamplePlaceholder(values) { return requiredSampleValuesForType(field.value.Type()) } return values, nil } func sampleValuesForType(t reflect.Type) ([]string, error) { for t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.Slice: return sampleScalarValue(t.Elem()) case reflect.Array: if t.Len() == 0 { return nil, nil } values := make([]string, 0, t.Len()) for idx := 0; idx < t.Len(); idx++ { items, err := sampleScalarValue(t.Elem()) if err != nil { return nil, err } values = append(values, items...) } return values, nil case reflect.Map: if t.Key().Kind() != reflect.String { return nil, fmt.Errorf("unsupported map key kind %s", t.Key().Kind()) } values, err := sampleScalarValue(t.Elem()) if err != nil { return nil, err } if len(values) == 0 { return nil, nil } return []string{"key=" + values[0]}, nil default: return sampleScalarValue(t) } } func sampleScalarValue(t reflect.Type) ([]string, error) { for t.Kind() == reflect.Ptr { t = t.Elem() } value := reflect.Zero(t) text, err := scalarToString(value) if err != nil { return nil, err } return []string{text}, nil } func needsRequiredSamplePlaceholder(values []string) bool { if len(values) == 0 { return true } for _, value := range values { if strings.TrimSpace(value) != "" { return false } } return true } func requiredSampleValuesForType(t reflect.Type) ([]string, error) { for t.Kind() == reflect.Ptr { t = t.Elem() } switch t.Kind() { case reflect.String: return []string{"value"}, nil case reflect.Slice: return requiredSampleValuesForType(t.Elem()) case reflect.Array: if t.Len() == 0 { return nil, nil } values := make([]string, 0, t.Len()) for idx := 0; idx < t.Len(); idx++ { items, err := requiredSampleValuesForType(t.Elem()) if err != nil { return nil, err } values = append(values, items...) } return values, nil case reflect.Map: if t.Key().Kind() != reflect.String { return nil, fmt.Errorf("unsupported map key kind %s", t.Key().Kind()) } values, err := requiredSampleValuesForType(t.Elem()) if err != nil { return nil, err } if needsRequiredSamplePlaceholder(values) { values, err = sampleValuesForType(t.Elem()) if err != nil { return nil, err } } if len(values) == 0 { return nil, nil } return []string{"key=" + values[0]}, nil default: return sampleValuesForType(t) } } func defaultValuesForField(field configField, text string) ([]string, error) { v := field.value for v.Kind() == reflect.Ptr { if v.IsNil() { v = reflect.New(v.Type().Elem()).Elem() break } v = v.Elem() } switch v.Kind() { case reflect.Slice, reflect.Array, reflect.Map: return splitFieldValue(text, field.split) } return []string{text}, nil } func splitFieldValue(text, split string) ([]string, error) { switch { case split == "": return []string{text}, nil case split == "," || strings.EqualFold(split, "csv"): return parseCSVList(text) default: return parseDelimitedList(text, split), nil } } func parseCSVList(text string) ([]string, error) { if text == "" { return []string{""}, nil } reader := csv.NewReader(strings.NewReader(text)) reader.TrimLeadingSpace = true record, err := reader.Read() if err != nil { return nil, err } for idx := range record { record[idx] = strings.TrimSpace(record[idx]) } return record, nil } func parseDelimitedList(text, split string) []string { if split == "" { return []string{text} } parts := strings.Split(text, split) for idx := range parts { parts[idx] = strings.TrimSpace(parts[idx]) } return parts } func setConfigField(value reflect.Value, section *Section, key, split string) error { for value.Kind() == reflect.Ptr { if value.IsNil() { value.Set(reflect.New(value.Type().Elem())) } value = value.Elem() } items, err := expandConfigValues(value, configValuesFromSection(section, key), split) if err != nil { return err } if len(items) == 0 { return nil } return setConfigValueItems(value, items) } func setConfigValueItems(value reflect.Value, items []configValue) error { switch value.Kind() { case reflect.Slice: return setConfigSlice(value, items) case reflect.Array: return setConfigArray(value, items) case reflect.Map: return setConfigMap(value, items) default: return setConfigScalar(value, items[0]) } } func expandConfigValues(value reflect.Value, items []configValue, split string) ([]configValue, error) { if split == "" { return items, nil } switch value.Kind() { case reflect.Slice, reflect.Array, reflect.Map: default: return items, nil } out := make([]configValue, 0, len(items)) for _, item := range items { if item.noValue { out = append(out, item) continue } parts, err := splitFieldValue(item.text, split) if err != nil { return nil, err } for _, part := range parts { out = append(out, configValue{text: part}) } } return out, nil } func configValuesFromSection(section *Section, key string) []configValue { return configValuesFromSections([]*Section{section}, key) } func configValuesFromSections(sections []*Section, key string) []configValue { values := make([]configValue, 0) for _, section := range sections { if section == nil { continue } entries := section.EntriesByKey(key) for _, entry := range entries { if entry == nil { continue } if entry.NoValue && len(entry.Values) == 0 { values = append(values, configValue{noValue: true}) continue } for _, value := range entry.Values { values = append(values, configValue{text: value}) } } } if len(values) == 0 { return nil } return values } func setConfigSlice(value reflect.Value, items []configValue) error { out := reflect.MakeSlice(value.Type(), 0, len(items)) for _, item := range items { elem := reflect.New(value.Type().Elem()).Elem() if err := setConfigScalar(elem, item); err != nil { return err } out = reflect.Append(out, elem) } value.Set(out) return nil } func setConfigArray(value reflect.Value, items []configValue) error { if len(items) != value.Len() { return fmt.Errorf("array needs %d values, got %d", value.Len(), len(items)) } for idx, item := range items { if err := setConfigScalar(value.Index(idx), item); err != nil { return err } } return nil } func setConfigMap(value reflect.Value, items []configValue) error { if value.Type().Key().Kind() != reflect.String { return fmt.Errorf("unsupported map key kind %s", value.Type().Key().Kind()) } out := reflect.MakeMapWithSize(value.Type(), len(items)) for idx, item := range items { key, text := parseMapItem(idx, item.text) elem := reflect.New(value.Type().Elem()).Elem() mapItem := configValue{text: text, noValue: item.noValue} if err := setConfigScalar(elem, mapItem); err != nil { return err } out.SetMapIndex(reflect.ValueOf(key), elem) } value.Set(out) return nil } func parseMapItem(idx int, text string) (string, string) { if at := strings.Index(text, "="); at >= 0 { return strings.TrimSpace(text[:at]), strings.TrimSpace(text[at+1:]) } return strconv.Itoa(idx), text } func outputValuesForField(field configField) ([]string, error) { value := field.value for value.Kind() == reflect.Ptr { if value.IsNil() { return nil, nil } value = value.Elem() } switch value.Kind() { case reflect.Slice, reflect.Array: values := make([]string, 0, value.Len()) for idx := 0; idx < value.Len(); idx++ { text, err := scalarToString(value.Index(idx)) if err != nil { return nil, err } values = append(values, text) } return values, nil case reflect.Map: if value.Type().Key().Kind() != reflect.String { return nil, fmt.Errorf("unsupported map key kind %s", value.Type().Key().Kind()) } keys := make([]string, 0, value.Len()) for _, key := range value.MapKeys() { keys = append(keys, key.String()) } sort.Strings(keys) values := make([]string, 0, len(keys)) for _, key := range keys { text, err := scalarToString(value.MapIndex(reflect.ValueOf(key))) if err != nil { return nil, err } values = append(values, key+"="+text) } return values, nil default: text, err := scalarToString(value) if err != nil { return nil, err } return []string{text}, nil } } func scalarToString(value reflect.Value) (string, error) { for value.Kind() == reflect.Ptr { if value.IsNil() { return "", nil } value = value.Elem() } if ok, text, err := textMarshalerString(value); ok || err != nil { return text, err } if value.Type() == reflect.TypeOf(time.Duration(0)) { return time.Duration(value.Int()).String(), nil } switch value.Kind() { case reflect.String: return value.String(), nil case reflect.Bool: return strconv.FormatBool(value.Bool()), nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return strconv.FormatInt(value.Int(), 10), nil case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: return strconv.FormatUint(value.Uint(), 10), nil case reflect.Float32, reflect.Float64: return strconv.FormatFloat(value.Float(), 'g', -1, value.Type().Bits()), nil default: return "", fmt.Errorf("unsupported field kind %s", value.Kind()) } } func textMarshalerString(value reflect.Value) (bool, string, error) { if value.CanInterface() { if marshaler, ok := value.Interface().(encoding.TextMarshaler); ok { data, err := marshaler.MarshalText() return true, string(data), err } } if value.CanAddr() { if marshaler, ok := value.Addr().Interface().(encoding.TextMarshaler); ok { data, err := marshaler.MarshalText() return true, string(data), err } } return false, "", nil } func setConfigScalar(value reflect.Value, item configValue) error { for value.Kind() == reflect.Ptr { if value.IsNil() { value.Set(reflect.New(value.Type().Elem())) } value = value.Elem() } if ok, err := setTextUnmarshaler(value, item.text); ok || err != nil { if err != nil { return err } return nil } if value.Type() == reflect.TypeOf(time.Duration(0)) { duration, err := time.ParseDuration(item.text) if err != nil { return err } value.SetInt(int64(duration)) return nil } switch value.Kind() { case reflect.String: value.SetString(item.text) case reflect.Bool: if item.noValue { value.SetBool(true) return nil } v, err := strconv.ParseBool(item.text) if err != nil { return err } value.SetBool(v) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: n, err := strconv.ParseInt(item.text, 10, value.Type().Bits()) if err != nil { return err } value.SetInt(n) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: n, err := strconv.ParseUint(item.text, 10, value.Type().Bits()) if err != nil { return err } value.SetUint(n) case reflect.Float32, reflect.Float64: n, err := strconv.ParseFloat(item.text, value.Type().Bits()) if err != nil { return err } value.SetFloat(n) default: return fmt.Errorf("unsupported field kind %s", value.Kind()) } return nil } func setTextUnmarshaler(value reflect.Value, text string) (bool, error) { if value.CanAddr() { if unmarshaler, ok := value.Addr().Interface().(encoding.TextUnmarshaler); ok { return true, unmarshaler.UnmarshalText([]byte(text)) } } if value.CanInterface() { if unmarshaler, ok := value.Interface().(encoding.TextUnmarshaler); ok { return true, unmarshaler.UnmarshalText([]byte(text)) } } return false, nil } func implementsTextUnmarshaler(t reflect.Type) bool { textUnmarshaler := reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() if t.Implements(textUnmarshaler) { return true } return reflect.PtrTo(t).Implements(textUnmarshaler) } func sectionHasPairs(section *Section) bool { if section == nil { return false } for _, entry := range section.Entries { if isConfigEntry(entry) { return true } } return false } func isConfigEntry(entry *Entry) bool { return entry != nil && entry.Key != "" } func configPath(section, key string) string { if section == "" { return key } return section + "." + key } func configFieldError(field configField, reason string) error { return configFieldErrorWithErr(field, reason, nil) } func configFieldErrorWithErr(field configField, reason string, err error) error { return &ConfigError{ Section: field.section, Key: field.key, Field: field.structField.Name, Reason: reason, Err: err, } } func configKeyError(section, key, reason string, err error) error { return &ConfigError{ Section: section, Key: key, Reason: reason, Err: err, } } func validateConfig(dst interface{}) error { if validator, ok := dst.(ConfigValidator); ok { return validator.Validate() } value := reflect.ValueOf(dst) if value.Kind() == reflect.Ptr && !value.IsNil() { if validator, ok := value.Elem().Interface().(ConfigValidator); ok { return validator.Validate() } } return nil }