...

Source file src/github.com/johnstarich/env2config/config.go

Documentation: github.com/johnstarich/env2config

     1  package env2config
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  	"unicode"
    10  
    11  	"github.com/kelseyhightower/envconfig"
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  type Config struct {
    16  	Name   string
    17  	Opts   Opts // NAME_OPTS_*
    18  	Values Values
    19  
    20  	registry *registry
    21  }
    22  
    23  type Opts struct {
    24  	File               string   `required:"true"`
    25  	Format             string   `required:"true"`
    26  	TemplateFile       string   `split_words:"true"`
    27  	TemplateDeleteKeys []string `split_words:"true"`
    28  
    29  	Inputs Values // NAME_OPTS_IN_*
    30  }
    31  
    32  type Values map[string]string
    33  
    34  var _ envconfig.Setter = Values{}
    35  
    36  func (v Values) Set(value string) error { return nil }
    37  
    38  func New(name string) (Config, error) {
    39  	c, err := newConfig(name, parseEnv(os.Environ()), defaultRegistry)
    40  	if name != "" {
    41  		err = errors.Wrap(err, strings.ToLower(name))
    42  	}
    43  	return c, err
    44  }
    45  
    46  func newConfig(name string, env map[string]string, registry *registry) (Config, error) {
    47  	name = strings.ToLower(name)
    48  	if name == "" {
    49  		return Config{}, errors.New("Config name is required")
    50  	}
    51  	trimmed := name
    52  	trimmed = strings.TrimFunc(trimmed, unicode.IsLetter)
    53  	trimmed = strings.TrimFunc(trimmed, unicode.IsNumber)
    54  	if trimmed != "" {
    55  		return Config{}, errors.Errorf("Config names must only use letters or numbers: %q", name)
    56  	}
    57  	config := Config{
    58  		registry: registry,
    59  	}
    60  	err := envconfig.Process(name, &config)
    61  	if err != nil {
    62  		return Config{}, err
    63  	}
    64  	config.Name = name
    65  	config.Opts.Inputs = filterEnvPrefix(name+"_opts_in", env)
    66  	config.Values = configEnvValues(name, env)
    67  
    68  	var missingInputs []string
    69  	for dest, src := range config.Opts.Inputs {
    70  		value, isSet := env[src]
    71  		if !isSet {
    72  			missingInputs = append(missingInputs, src)
    73  		}
    74  		config.Values[dest] = value
    75  	}
    76  	if len(missingInputs) > 0 {
    77  		sort.Strings(missingInputs)
    78  		return Config{}, errors.Errorf("Missing required environment variables: %s", strings.Join(missingInputs, ", "))
    79  	}
    80  	return config, nil
    81  }
    82  
    83  func (c Config) Write() error {
    84  	var template map[string]interface{}
    85  	if c.Opts.TemplateFile != "" {
    86  		err := os.MkdirAll(filepath.Dir(c.Opts.TemplateFile), 0755)
    87  		if err != nil {
    88  			return err
    89  		}
    90  		f, err := os.Open(c.Opts.TemplateFile)
    91  		if err != nil {
    92  			return err
    93  		}
    94  		defer f.Close()
    95  		err = c.registry.UnmarshalFormat(c.Opts.Format, f, &template)
    96  		if err != nil {
    97  			return err
    98  		}
    99  		sortTemplateDeleteKeys(c.Opts.TemplateDeleteKeys)
   100  		for _, deleteKey := range c.Opts.TemplateDeleteKeys {
   101  			templateInt, _ := deleteKeyPath(template, parseKeyPath(deleteKey))
   102  			template = templateInt.(map[string]interface{})
   103  		}
   104  	}
   105  	values := c.writableValues(template)
   106  	f, err := os.OpenFile(c.Opts.File, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
   107  	if err != nil {
   108  		return err
   109  	}
   110  	defer f.Close()
   111  	return c.registry.MarshalFormat(c.Opts.Format, f, values)
   112  }
   113  
   114  func (c Config) writableValues(template map[string]interface{}) interface{} {
   115  	result := template
   116  	if result == nil {
   117  		result = make(map[string]interface{})
   118  	}
   119  	for key, value := range c.Values {
   120  		current := result
   121  		keys := parseKeyPath(key)
   122  		for i := 0; i < len(keys)-1; i++ {
   123  			key := keys[i]
   124  			_, exists := current[key]
   125  			if !exists {
   126  				current[key] = make(map[string]interface{})
   127  			}
   128  			switch next := current[key].(type) {
   129  			case map[string]interface{}:
   130  				current = next
   131  			case []interface{}:
   132  				nextMap := arrayToMap(next)
   133  				current[key] = nextMap
   134  				current = nextMap
   135  			default:
   136  				// unrecognized type, just do simple override
   137  				nextMap := make(map[string]interface{})
   138  				current[key] = nextMap
   139  				current = nextMap
   140  			}
   141  		}
   142  		lastKey := keys[len(keys)-1]
   143  		current[lastKey] = value
   144  	}
   145  
   146  	return mapsToArrays(result)
   147  }
   148  
   149  func mapsToArrays(m map[string]interface{}) interface{} {
   150  	isArray := true
   151  	for key, value := range m {
   152  		if mapValue, isMap := value.(map[string]interface{}); isMap {
   153  			m[key] = mapsToArrays(mapValue)
   154  		}
   155  		if strings.TrimFunc(key, unicode.IsNumber) != "" {
   156  			isArray = false
   157  		}
   158  	}
   159  	if !isArray {
   160  		return m
   161  	}
   162  	// Must be an array, or empty set.
   163  	// Empty set shouldn't be possible based on logic of writableValues().
   164  	values := make([]interface{}, len(m))
   165  	for key, value := range m {
   166  		index, err := strconv.ParseInt(key, 10, 64)
   167  		if err != nil {
   168  			panic(err) // string is all digits so must parse as int
   169  		}
   170  		values[index] = value
   171  	}
   172  	return values
   173  }
   174  
   175  func arrayToMap(a []interface{}) map[string]interface{} {
   176  	m := make(map[string]interface{}, len(a))
   177  	for index, value := range a {
   178  		m[strconv.FormatInt(int64(index), 10)] = value
   179  	}
   180  	return m
   181  }
   182