204 lines
5.7 KiB
Go
204 lines
5.7 KiB
Go
|
package configuration
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"os"
|
||
|
"reflect"
|
||
|
"regexp"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
|
||
|
"gopkg.in/BrianBland/yaml.v2"
|
||
|
)
|
||
|
|
||
|
// Version is a major/minor version pair of the form Major.Minor
|
||
|
// Major version upgrades indicate structure or type changes
|
||
|
// Minor version upgrades should be strictly additive
|
||
|
type Version string
|
||
|
|
||
|
// MajorMinorVersion constructs a Version from its Major and Minor components
|
||
|
func MajorMinorVersion(major, minor uint) Version {
|
||
|
return Version(fmt.Sprintf("%d.%d", major, minor))
|
||
|
}
|
||
|
|
||
|
func (version Version) major() (uint, error) {
|
||
|
majorPart := strings.Split(string(version), ".")[0]
|
||
|
major, err := strconv.ParseUint(majorPart, 10, 0)
|
||
|
return uint(major), err
|
||
|
}
|
||
|
|
||
|
// Major returns the major version portion of a Version
|
||
|
func (version Version) Major() uint {
|
||
|
major, _ := version.major()
|
||
|
return major
|
||
|
}
|
||
|
|
||
|
func (version Version) minor() (uint, error) {
|
||
|
minorPart := strings.Split(string(version), ".")[1]
|
||
|
minor, err := strconv.ParseUint(minorPart, 10, 0)
|
||
|
return uint(minor), err
|
||
|
}
|
||
|
|
||
|
// Minor returns the minor version portion of a Version
|
||
|
func (version Version) Minor() uint {
|
||
|
minor, _ := version.minor()
|
||
|
return minor
|
||
|
}
|
||
|
|
||
|
// VersionedParseInfo defines how a specific version of a configuration should
|
||
|
// be parsed into the current version
|
||
|
type VersionedParseInfo struct {
|
||
|
// Version is the version which this parsing information relates to
|
||
|
Version Version
|
||
|
// ParseAs defines the type which a configuration file of this version
|
||
|
// should be parsed into
|
||
|
ParseAs reflect.Type
|
||
|
// ConversionFunc defines a method for converting the parsed configuration
|
||
|
// (of type ParseAs) into the current configuration version
|
||
|
// Note: this method signature is very unclear with the absence of generics
|
||
|
ConversionFunc func(interface{}) (interface{}, error)
|
||
|
}
|
||
|
|
||
|
// Parser can be used to parse a configuration file and environment of a defined
|
||
|
// version into a unified output structure
|
||
|
type Parser struct {
|
||
|
prefix string
|
||
|
mapping map[Version]VersionedParseInfo
|
||
|
env map[string]string
|
||
|
}
|
||
|
|
||
|
// NewParser returns a *Parser with the given environment prefix which handles
|
||
|
// versioned configurations which match the given parseInfos
|
||
|
func NewParser(prefix string, parseInfos []VersionedParseInfo) *Parser {
|
||
|
p := Parser{prefix: prefix, mapping: make(map[Version]VersionedParseInfo), env: make(map[string]string)}
|
||
|
|
||
|
for _, parseInfo := range parseInfos {
|
||
|
p.mapping[parseInfo.Version] = parseInfo
|
||
|
}
|
||
|
|
||
|
for _, env := range os.Environ() {
|
||
|
envParts := strings.SplitN(env, "=", 2)
|
||
|
p.env[envParts[0]] = envParts[1]
|
||
|
}
|
||
|
|
||
|
return &p
|
||
|
}
|
||
|
|
||
|
// Parse reads in the given []byte and environment and writes the resulting
|
||
|
// configuration into the input v
|
||
|
//
|
||
|
// Environment variables may be used to override configuration parameters other
|
||
|
// than version, following the scheme below:
|
||
|
// v.Abc may be replaced by the value of PREFIX_ABC,
|
||
|
// v.Abc.Xyz may be replaced by the value of PREFIX_ABC_XYZ, and so forth
|
||
|
func (p *Parser) Parse(in []byte, v interface{}) error {
|
||
|
var versionedStruct struct {
|
||
|
Version Version
|
||
|
}
|
||
|
|
||
|
if err := yaml.Unmarshal(in, &versionedStruct); err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
parseInfo, ok := p.mapping[versionedStruct.Version]
|
||
|
if !ok {
|
||
|
return fmt.Errorf("Unsupported version: %q", versionedStruct.Version)
|
||
|
}
|
||
|
|
||
|
parseAs := reflect.New(parseInfo.ParseAs)
|
||
|
err := yaml.Unmarshal(in, parseAs.Interface())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
err = p.overwriteFields(parseAs, p.prefix)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
c, err := parseInfo.ConversionFunc(parseAs.Interface())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
reflect.ValueOf(v).Elem().Set(reflect.Indirect(reflect.ValueOf(c)))
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (p *Parser) overwriteFields(v reflect.Value, prefix string) error {
|
||
|
for v.Kind() == reflect.Ptr {
|
||
|
v = reflect.Indirect(v)
|
||
|
}
|
||
|
switch v.Kind() {
|
||
|
case reflect.Struct:
|
||
|
for i := 0; i < v.NumField(); i++ {
|
||
|
sf := v.Type().Field(i)
|
||
|
fieldPrefix := strings.ToUpper(prefix + "_" + sf.Name)
|
||
|
if e, ok := p.env[fieldPrefix]; ok {
|
||
|
fieldVal := reflect.New(sf.Type)
|
||
|
err := yaml.Unmarshal([]byte(e), fieldVal.Interface())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
v.Field(i).Set(reflect.Indirect(fieldVal))
|
||
|
}
|
||
|
err := p.overwriteFields(v.Field(i), fieldPrefix)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
case reflect.Map:
|
||
|
p.overwriteMap(v, prefix)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (p *Parser) overwriteMap(m reflect.Value, prefix string) error {
|
||
|
switch m.Type().Elem().Kind() {
|
||
|
case reflect.Struct:
|
||
|
for _, k := range m.MapKeys() {
|
||
|
err := p.overwriteFields(m.MapIndex(k), strings.ToUpper(fmt.Sprintf("%s_%s", prefix, k)))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
envMapRegexp, err := regexp.Compile(fmt.Sprintf("^%s_([A-Z0-9]+)$", strings.ToUpper(prefix)))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
for key, val := range p.env {
|
||
|
if submatches := envMapRegexp.FindStringSubmatch(key); submatches != nil {
|
||
|
mapValue := reflect.New(m.Type().Elem())
|
||
|
err := yaml.Unmarshal([]byte(val), mapValue.Interface())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
m.SetMapIndex(reflect.ValueOf(strings.ToLower(submatches[1])), reflect.Indirect(mapValue))
|
||
|
}
|
||
|
}
|
||
|
case reflect.Map:
|
||
|
for _, k := range m.MapKeys() {
|
||
|
err := p.overwriteMap(m.MapIndex(k), strings.ToUpper(fmt.Sprintf("%s_%s", prefix, k)))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
default:
|
||
|
envMapRegexp, err := regexp.Compile(fmt.Sprintf("^%s_([A-Z0-9]+)$", strings.ToUpper(prefix)))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
for key, val := range p.env {
|
||
|
if submatches := envMapRegexp.FindStringSubmatch(key); submatches != nil {
|
||
|
mapValue := reflect.New(m.Type().Elem())
|
||
|
err := yaml.Unmarshal([]byte(val), mapValue.Interface())
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
m.SetMapIndex(reflect.ValueOf(strings.ToLower(submatches[1])), reflect.Indirect(mapValue))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|