github.com/blend/go-sdk@v1.20240719.1/configutil/read.go (about)

     1  /*
     2  
     3  Copyright (c) 2024 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package configutil
     9  
    10  import (
    11  	"encoding/json"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  
    17  	"gopkg.in/yaml.v3"
    18  
    19  	"github.com/blend/go-sdk/env"
    20  	"github.com/blend/go-sdk/ex"
    21  )
    22  
    23  // MustRead reads a config from optional path(s) and panics on error.
    24  //
    25  // It is functionally equivalent to `Read` outside error handling; see this function for more information.
    26  func MustRead(ref Any, options ...Option) (filePaths []string) {
    27  	var err error
    28  	filePaths, err = Read(ref, options...)
    29  	if !IsIgnored(err) {
    30  		panic(err)
    31  	}
    32  	return
    33  }
    34  
    35  // 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.
    36  /*
    37  If the ref type is a `Resolver` the `Resolve(context.Context) error` method will
    38  be called on the ref and passed a context configured from the given options.
    39  
    40  By default, a well known set of paths will be read from (including a path read from the environment variable `CONFIG_PATH`).
    41  
    42  You can override this by providing options to specify which paths will be read from:
    43  
    44  	paths, err := configutil.Read(&cfg, configutil.OptPaths("foo.yml"))
    45  
    46  The above will _only_ read from `foo.yml` to populate the `cfg` reference.
    47  */
    48  func Read(ref Any, options ...Option) (paths []string, err error) {
    49  	var configOptions ConfigOptions
    50  	configOptions, err = createConfigOptions(options...)
    51  	if err != nil {
    52  		return
    53  	}
    54  
    55  	for _, contents := range configOptions.Contents {
    56  		MaybeDebugf(configOptions.Log, "reading config contents with extension `%s`", contents.Ext)
    57  		err = deserialize(contents.Ext, contents.Contents, ref)
    58  		if err != nil {
    59  			return
    60  		}
    61  	}
    62  
    63  	var f *os.File
    64  	var path string
    65  	var resolveErr error
    66  	for _, path = range configOptions.FilePaths {
    67  		if path == "" {
    68  			continue
    69  		}
    70  		MaybeDebugf(configOptions.Log, "checking for config path: %s", path)
    71  		f, resolveErr = os.Open(path)
    72  		if IsNotExist(resolveErr) {
    73  			continue
    74  		}
    75  		if resolveErr != nil {
    76  			err = ex.New(resolveErr)
    77  			break
    78  		}
    79  		defer f.Close()
    80  
    81  		MaybeDebugf(configOptions.Log, "reading config path: %s", path)
    82  		resolveErr = deserialize(filepath.Ext(path), f, ref)
    83  		if resolveErr != nil {
    84  			err = ex.New(resolveErr)
    85  			return
    86  		}
    87  
    88  		paths = append(paths, path)
    89  	}
    90  
    91  	if typed, ok := ref.(Resolver); ok {
    92  		MaybeDebugf(configOptions.Log, "calling config resolver")
    93  		if resolveErr := typed.Resolve(configOptions.Background()); resolveErr != nil {
    94  			MaybeErrorf(configOptions.Log, "calling resolver error: %+v", resolveErr)
    95  			err = resolveErr
    96  			return
    97  		}
    98  	}
    99  	return
   100  }
   101  
   102  func createConfigOptions(options ...Option) (configOptions ConfigOptions, err error) {
   103  	configOptions.Env = env.Env()
   104  	configOptions.FilePaths = DefaultPaths
   105  	if configOptions.Env.Has(EnvVarConfigPath) {
   106  		configOptions.FilePaths = append(configOptions.Env.CSV(EnvVarConfigPath), configOptions.FilePaths...)
   107  	}
   108  	for _, option := range options {
   109  		if err = option(&configOptions); err != nil {
   110  			return
   111  		}
   112  	}
   113  	return
   114  }
   115  
   116  // deserialize deserializes a config.
   117  func deserialize(ext string, r io.Reader, ref Any) error {
   118  	// make sure the extension starts with a "."
   119  	if !strings.HasPrefix(ext, ".") {
   120  		ext = "." + ext
   121  	}
   122  
   123  	// based off the extension, use the appropriate deserializer
   124  	switch strings.ToLower(ext) {
   125  	case ExtensionJSON:
   126  		return ex.New(json.NewDecoder(r).Decode(ref))
   127  	case ExtensionYAML, ExtensionYML:
   128  		return ex.New(yaml.NewDecoder(r).Decode(ref))
   129  	default: // return an error if we're passed a weird extension
   130  		return ex.New(ErrInvalidConfigExtension, ex.OptMessagef("extension: %s", ext))
   131  	}
   132  }