1717 lines
38 KiB
Go
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
|
||
|
|
}
|