staros/sysconf/config.go
starainrt d93a851d1b
feat: 完善 staros 系统能力并更新 wincmd 发布版依赖
- 重构 sysconf 为文档模型 INI Parser 与 Config Framework
- 强化 hosts 解析、插入校验、写回与异常输入处理
- 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向
- 扩展跨平台文件时间、文件锁、内存、进程与网络能力
- 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0
- 移除本地 wincmd/win32api replace,改用发布版依赖
- 将最低 Go 版本提升到 1.18
- 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
2026-06-09 18:10:19 +08:00

1717 lines
38 KiB
Go

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
}