go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/configutil/read.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package configutil
     9  
    10  import (
    11  	"encoding/json"
    12  	"io"
    13  	"os"
    14  
    15  	"gopkg.in/yaml.v3"
    16  )
    17  
    18  // MustRead reads a config from optional path(s) and panics on error.
    19  //
    20  // It is functionally equivalent to `Read` outside error handling; see this function for more information.
    21  func MustRead(ref any, options ...Option) (filePaths []string) {
    22  	var err error
    23  	filePaths, err = Read(ref, options...)
    24  	if !IsIgnored(err) {
    25  		panic(err)
    26  	}
    27  	return
    28  }
    29  
    30  /*
    31  Read reads a config from optional path(s), returning the paths read from (in the order visited), and an error if there were any issues.
    32  
    33  If the ref type is a `Resolver` the `Resolve(context.Context) error` method will
    34  be called on the ref and passed a context configured from the given options.
    35  
    36  By default, a well known set of paths will be read from (including a path read from the environment variable `CONFIG_PATH`).
    37  
    38  You can override this by providing options to specify which paths will be read from:
    39  
    40  	paths, err := configutil.Read(&cfg, configutil.OptPaths("foo.yml"))
    41  
    42  The above will _only_ read from `foo.yml` to populate the `cfg` reference.
    43  */
    44  func Read(ref any, options ...Option) (paths []string, err error) {
    45  	var configOptions ConfigOptions
    46  	configOptions, err = createConfigOptions(options...)
    47  	if err != nil {
    48  		return
    49  	}
    50  
    51  	for _, contents := range configOptions.Contents {
    52  		err = configOptions.Deserializer(contents, ref)
    53  		if err != nil {
    54  			return
    55  		}
    56  	}
    57  
    58  	var f *os.File
    59  	var path string
    60  	var resolveErr error
    61  	for _, path = range configOptions.FilePaths {
    62  		if path == "" {
    63  			continue
    64  		}
    65  		f, resolveErr = os.Open(path)
    66  		if IsNotExist(resolveErr) {
    67  			continue
    68  		}
    69  		if resolveErr != nil {
    70  			err = resolveErr
    71  			break
    72  		}
    73  		defer f.Close()
    74  
    75  		resolveErr = configOptions.Deserializer(f, ref)
    76  		if resolveErr != nil {
    77  			err = resolveErr
    78  			return
    79  		}
    80  
    81  		paths = append(paths, path)
    82  	}
    83  
    84  	if typed, ok := ref.(Resolver); ok {
    85  		if resolveErr := typed.Resolve(configOptions.Background()); resolveErr != nil {
    86  			err = resolveErr
    87  			return
    88  		}
    89  	}
    90  	return
    91  }
    92  
    93  func createConfigOptions(options ...Option) (configOptions ConfigOptions, err error) {
    94  	configOptions.Env = parseEnv()
    95  	configOptions.FilePaths = DefaultPaths
    96  	configOptions.Deserializer = deserializeYAML
    97  	if configPath, ok := configOptions.Env[EnvVarConfigPath]; ok && configPath != "" {
    98  		configOptions.FilePaths = []string{configPath}
    99  	}
   100  	for _, option := range options {
   101  		if err = option(&configOptions); err != nil {
   102  			return
   103  		}
   104  	}
   105  	return
   106  }
   107  
   108  func deserializeJSON(r io.Reader, ref any) error {
   109  	return json.NewDecoder(r).Decode(ref)
   110  }
   111  
   112  func deserializeYAML(r io.Reader, ref any) error {
   113  	return yaml.NewDecoder(r).Decode(ref)
   114  }
   115  
   116  func parseEnv() map[string]string {
   117  	var key, value string
   118  	vars := make(map[string]string)
   119  	for _, ev := range os.Environ() {
   120  		key, value = splitVar(ev)
   121  		if key != "" {
   122  			vars[key] = value
   123  		}
   124  	}
   125  	return vars
   126  }
   127  
   128  func splitVar(s string) (key, value string) {
   129  	for i := 0; i < len(s); i++ {
   130  		if s[i] == '=' {
   131  			key = s[:i]
   132  			value = s[i+1:]
   133  			return
   134  		}
   135  	}
   136  	return
   137  }