package cfger import ( "errors" "fmt" "os" "regexp" "strconv" "strings" "gopkg.in/yaml.v3" ) // FatalErrorHandler is called with a fatal error before cfger calls os.Exit. // This is to allow fatal errors to be processed correctly depending on the // application itself. var FatalErrorHandler = func(err error) { _, _ = fmt.Fprintf(os.Stderr, "%v\n", err) } type ConfigLoader struct { rawConfigFileContents map[string]any lastKey string } func New() *ConfigLoader { return &ConfigLoader{} } // Load reads a YAML config file from fname and holds it in memory. // // If fname cannot be found, a blank config file will be loaded // instead. func (cl *ConfigLoader) Load(fname string) error { cl.rawConfigFileContents = make(map[string]any) fcont, err := os.ReadFile(fname) if err != nil { if errors.Is(err, os.ErrNotExist) { return nil } else { return err } } if err := yaml.Unmarshal(fcont, &cl.rawConfigFileContents); err != nil { return fmt.Errorf("cfger: unmarshal %s: %w", fname, err) } return nil } var indexedPartRegexp = regexp.MustCompile(`(?m)([a-zA-Z]+)(?:\[(\d+)\])?`) // Get gets a given key from the currently loaded configuration. // // Within a key representation, a dot represents a recursion into a named // object and square brackets represent an index in a named array. You could access the value "hello" using the key `alpha[0].beta` in the below example. // // alpha: // - beta: "hello" func (cl *ConfigLoader) Get(key string) OptionalItem { // httpcore[2].bananas parts := strings.Split(key, ".") var cursor any = cl.rawConfigFileContents for _, part := range parts { components := indexedPartRegexp.FindStringSubmatch(part) key := components[1] index, _ := strconv.ParseInt(components[2], 10, 32) isIndexed := components[2] != "" item, found := cursor.(map[string]any)[key] if !found { return OptionalItem{key, nil, false} } if isIndexed { arr, conversionOk := item.([]any) if !conversionOk { panic(fmt.Errorf("cfger: attempted to index non-indexable config item %s", key)) } cursor = arr[index] } else { cursor = item } } return OptionalItem{key, cursor, true} } // GetEnv gets a given key from the environment variables. func (cl *ConfigLoader) GetEnv(envKey string) OptionalItem { ev := os.Getenv(envKey) if ev == "" { return OptionalItem{envKey, nil, false} } return OptionalItem{envKey, ev, true} }