Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,12 +131,15 @@ if err != nil { // Handle errors reading the config file
You can handle the specific case where no config file is found like this:

```go
var fileLookupError viper.FileLookupError
if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
// Config file not found; ignore error if desired
} else {
// Config file was found but another error was produced
}
if errors.As(err, &fileLookupError) {
// Indicates an explicitly set config file is not found (such as with
// using `viper.SetConfigFile`) or that no config file was found in
// any search path (such as when using `viper.AddConfigPath`)
} else {
// Config file was found but another error was produced
}
}

// Config file found and successfully parsed
Expand Down
93 changes: 93 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package viper

import (
"fmt"
)

// FileLookupError is returned when Viper cannot resolve a configuration file.
//
// This is meant to be a common interface for all file look-up errors, occurring either because a
// file does not exist or because it cannot find any file matching finder criteria.
type FileLookupError interface {
error

fileLookup()
}

// ConfigFileNotFoundError denotes failing to find a configuration file from a search.
//
// Deprecated: This is error wraps [FileNotFoundFromSearchError], which should be used instead.
type ConfigFileNotFoundError struct {
locations []string
name string
}

// Error returns the formatted error.
func (e ConfigFileNotFoundError) Error() string {
return e.Unwrap().Error()
}

// Unwraps to FileNotFoundFromSearchError.
func (e ConfigFileNotFoundError) Unwrap() error {
return FileNotFoundFromSearchError(e)
}

// FileNotFoundFromSearchError denotes failing to find a configuration file from a search.
// Wraps ConfigFileNotFoundError.
type FileNotFoundFromSearchError struct {
locations []string
name string
}

func (e FileNotFoundFromSearchError) fileLookup() {}

// Error returns the formatted error.
func (e FileNotFoundFromSearchError) Error() string {
message := fmt.Sprintf("File %q not found", e.name)

if len(e.locations) > 0 {
message += fmt.Sprintf(" in %v", e.locations)
}

return message
}

// FileNotFoundError denotes failing to find a specific configuration file.
type FileNotFoundError struct {
err error
path string
}

func (e FileNotFoundError) fileLookup() {}

// Error returns the formatted error.
func (e FileNotFoundError) Error() string {
return fmt.Sprintf("file not found: %s", e.path)
}

// ConfigFileAlreadyExistsError denotes failure to write new configuration file.
type ConfigFileAlreadyExistsError string

// Error returns the formatted error when configuration already exists.
func (e ConfigFileAlreadyExistsError) Error() string {
return fmt.Sprintf("Config File %q Already Exists", string(e))
}

// ConfigMarshalError happens when failing to marshal the configuration.
type ConfigMarshalError struct {
err error
}

// Error returns the formatted configuration error.
func (e ConfigMarshalError) Error() string {
return fmt.Sprintf("While marshaling config: %s", e.err.Error())
}

// UnsupportedConfigError denotes encountering an unsupported
// configuration filetype.
type UnsupportedConfigError string

// Error returns the formatted configuration error.
func (str UnsupportedConfigError) Error() string {
return fmt.Sprintf("Unsupported Config Type %q", string(str))
}
5 changes: 2 additions & 3 deletions file.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package viper

import (
"fmt"
"os"
"path/filepath"

Expand Down Expand Up @@ -50,7 +49,7 @@ func (v *Viper) findConfigFileWithFinder(finder Finder) (string, error) {
}

if len(results) == 0 {
return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
return "", ConfigFileNotFoundError{name: v.configName, locations: v.configPaths}
}

// We call clean on the final result to ensure that the path is in its canonical form.
Expand All @@ -69,7 +68,7 @@ func (v *Viper) findConfigFileOld() (string, error) {
return file, nil
}
}
return "", ConfigFileNotFoundError{v.configName, fmt.Sprintf("%s", v.configPaths)}
return "", ConfigFileNotFoundError{name: v.configName, locations: v.configPaths}
}

func (v *Viper) searchInPath(in string) (filename string) {
Expand Down
45 changes: 7 additions & 38 deletions viper.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"errors"
"fmt"
"io"
fs "io/fs"
"log/slog"
"os"
"path/filepath"
Expand All @@ -44,49 +45,12 @@ import (
"github.com/spf13/viper/internal/features"
)

// ConfigMarshalError happens when failing to marshal the configuration.
type ConfigMarshalError struct {
err error
}

// Error returns the formatted configuration error.
func (e ConfigMarshalError) Error() string {
return fmt.Sprintf("While marshaling config: %s", e.err.Error())
}

var v *Viper

func init() {
v = New()
}

// UnsupportedConfigError denotes encountering an unsupported
// configuration filetype.
type UnsupportedConfigError string

// Error returns the formatted configuration error.
func (str UnsupportedConfigError) Error() string {
return fmt.Sprintf("Unsupported Config Type %q", string(str))
}

// ConfigFileNotFoundError denotes failing to find configuration file.
type ConfigFileNotFoundError struct {
name, locations string
}

// Error returns the formatted configuration error.
func (fnfe ConfigFileNotFoundError) Error() string {
return fmt.Sprintf("Config File %q Not Found in %q", fnfe.name, fnfe.locations)
}

// ConfigFileAlreadyExistsError denotes failure to write new configuration file.
type ConfigFileAlreadyExistsError string

// Error returns the formatted error when configuration already exists.
func (faee ConfigFileAlreadyExistsError) Error() string {
return fmt.Sprintf("Config File %q Already Exists", string(faee))
}

// A DecoderConfigOption can be passed to viper.Unmarshal to configure
// mapstructure.DecoderConfig options.
type DecoderConfigOption func(*mapstructure.DecoderConfig)
Expand Down Expand Up @@ -1527,8 +1491,13 @@ func (v *Viper) ReadInConfig() error {
}

v.logger.Debug("reading file", "file", filename)

file, err := afero.ReadFile(v.fs, filename)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
// The specified config file is missing
return FileNotFoundError{err: err, path: filename}
} else if err != nil {
// We hit some other error from the filesystem that isn't a missing file
return err
}

Expand Down
40 changes: 37 additions & 3 deletions viper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,7 @@ func TestReadInConfig(t *testing.T) {
t.Run("config file set", func(t *testing.T) {
fs := afero.NewMemMapFs()

err := fs.Mkdir("/etc/viper", 0o777)
err := fs.Mkdir(testutil.AbsFilePath(t, "/etc/viper"), 0o777)
require.NoError(t, err)

file, err := fs.Create(testutil.AbsFilePath(t, "/etc/viper/config.yaml"))
Expand Down Expand Up @@ -1573,6 +1573,28 @@ func TestReadConfigWithSetConfigFile(t *testing.T) {
assert.Equal(t, 45000, v.GetInt("hello.pop"))
}

func TestWrongFileNotFound(t *testing.T) {
_, config := initDirs(t)

v := New()
v.SetConfigName(config)
v.SetDefault(`key`, `default`)

v.SetConfigFile(`whatareyoutalkingabout.yaml`)

err := v.ReadInConfig()

var fileLookupError FileLookupError

// It matches all error types and the shared error interface.
assert.ErrorAs(t, err, &FileNotFoundError{})
assert.ErrorAs(t, err, &fileLookupError)

// Even though config did not load and the error might have
// been ignored by the client, the default still loads
assert.Equal(t, `default`, v.GetString(`key`))
}

func TestIsSet(t *testing.T) {
v := New()
v.SetConfigType("yaml")
Expand Down Expand Up @@ -1652,7 +1674,13 @@ func TestWrongDirsSearchNotFound(t *testing.T) {
v.AddConfigPath(`thispathaintthere`)

err := v.ReadInConfig()
assert.IsType(t, ConfigFileNotFoundError{"", ""}, err)

var fileLookupError FileLookupError

// It matches all error types and the shared error interface.
assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
assert.ErrorAs(t, err, &fileLookupError)

// Even though config did not load and the error might have
// been ignored by the client, the default still loads
Expand All @@ -1670,7 +1698,13 @@ func TestWrongDirsSearchNotFoundForMerge(t *testing.T) {
v.AddConfigPath(`thispathaintthere`)

err := v.MergeInConfig()
assert.Equal(t, reflect.TypeOf(ConfigFileNotFoundError{"", ""}), reflect.TypeOf(err))

var fileLookupError FileLookupError

// It matches both types of errors.
assert.ErrorAs(t, err, &ConfigFileNotFoundError{})
assert.ErrorAs(t, err, &FileNotFoundFromSearchError{})
assert.ErrorAs(t, err, &fileLookupError)

// Even though config did not load and the error might have
// been ignored by the client, the default still loads
Expand Down
Loading