mirror of
https://github.com/aykhans/sarin.git
synced 2026-10-17 06:53:52 +00:00
fix(config): report YAML config errors more clearly
This commit is contained in:
4 files changed
+80
-15
No files matched your search
@@ -30,20 +30,11 @@ func (size *byteSize) Set(value string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalYAML accepts both a YAML integer and a string with a unit.
|
// UnmarshalYAML reads the raw scalar so a size parses the same way here as on the command line.
|
||||||
func (size *byteSize) UnmarshalYAML(node *yaml.Node) error {
|
func (size *byteSize) UnmarshalYAML(node *yaml.Node) error {
|
||||||
if node.Kind != yaml.ScalarNode {
|
if node.Kind != yaml.ScalarNode {
|
||||||
return fmt.Errorf("expected a size, but got %s", node.Tag)
|
return fmt.Errorf("expected a size, but got %s", node.Tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
if node.Tag == "!!int" {
|
|
||||||
var parsed uint64
|
|
||||||
if err := node.Decode(&parsed); err != nil {
|
|
||||||
return types.ErrByteSizeInvalid
|
|
||||||
}
|
|
||||||
*size = byteSize(parsed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return size.Set(node.Value)
|
return size.Set(node.Value)
|
||||||
}
|
}
|
||||||
@@ -652,12 +652,19 @@ func ReadAllConfigs() *Config {
|
|||||||
|
|
||||||
for _, configFile := range append(envConfig.Files, cliConf.Files...) {
|
for _, configFile := range append(envConfig.Files, cliConf.Files...) {
|
||||||
fileConfig, err := parseConfigFile(configFile, 10)
|
fileConfig, err := parseConfigFile(configFile, 10)
|
||||||
|
|
||||||
|
// A nested configFile fails under its own name, not the one given on the command line.
|
||||||
|
path := configFile.Path()
|
||||||
|
if configFileErr, ok := errors.AsType[types.ConfigFileError](err); ok {
|
||||||
|
path = configFileErr.Path
|
||||||
|
}
|
||||||
|
|
||||||
_ = utilsErr.MustHandle(err,
|
_ = utilsErr.MustHandle(err,
|
||||||
utilsErr.OnType(func(err types.ConfigFileReadError) error {
|
utilsErr.OnType(func(err types.ConfigFileReadError) error {
|
||||||
cliParser.PrintHelp()
|
cliParser.PrintHelp()
|
||||||
fmt.Fprint(os.Stderr, lipgloss.Sprintln(
|
fmt.Fprint(os.Stderr, lipgloss.Sprintln(
|
||||||
StyleYellow.Render(
|
StyleYellow.Render(
|
||||||
fmt.Sprintf("\nFailed to read config file (%s): ", configFile.Path())+err.Error(),
|
fmt.Sprintf("\nFailed to read config file (%s): ", path)+err.Error(),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -666,14 +673,14 @@ func ReadAllConfigs() *Config {
|
|||||||
utilsErr.OnType(func(err types.UnmarshalError) error {
|
utilsErr.OnType(func(err types.UnmarshalError) error {
|
||||||
fmt.Fprint(os.Stderr, lipgloss.Sprintln(
|
fmt.Fprint(os.Stderr, lipgloss.Sprintln(
|
||||||
StyleYellow.Render(
|
StyleYellow.Render(
|
||||||
fmt.Sprintf("\nFailed to parse config file (%s): ", configFile.Path())+err.Error(),
|
fmt.Sprintf("\nFailed to parse config file (%s): ", path)+err.Error(),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
utilsErr.OnType(func(err types.FieldParseErrors) error {
|
utilsErr.OnType(func(err types.FieldParseErrors) error {
|
||||||
printParseErrors(fmt.Sprintf("CONFIG FILE '%s'", configFile.Path()), err.Errors...)
|
printParseErrors(fmt.Sprintf("CONFIG FILE '%s'", path), err.Errors...)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return nil
|
return nil
|
||||||
}),
|
}),
|
||||||
@@ -697,7 +704,7 @@ func parseConfigFile(configFile types.ConfigFile, maxDepth int) (*Config, error)
|
|||||||
configFileParser := NewConfigFileParser(configFile)
|
configFileParser := NewConfigFileParser(configFile)
|
||||||
fileConfig, err := configFileParser.Parse()
|
fileConfig, err := configFileParser.Parse()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, types.NewConfigFileError(configFile.Path(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if maxDepth <= 0 {
|
if maxDepth <= 0 {
|
||||||
|
|||||||
+45
-1
@@ -3,12 +3,14 @@ package config
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -240,7 +242,15 @@ func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
|
|||||||
decoder.KnownFields(true)
|
decoder.KnownFields(true)
|
||||||
// The decoder returns a bare io.EOF for an empty file, errors.Is would also match a wrapped one.
|
// The decoder returns a bare io.EOF for an empty file, errors.Is would also match a wrapped one.
|
||||||
if err := decoder.Decode(parsedData); err != nil && err != io.EOF { //nolint:errorlint
|
if err := decoder.Decode(parsedData); err != nil && err != io.EOF { //nolint:errorlint
|
||||||
return nil, types.NewUnmarshalError(err)
|
return nil, types.NewUnmarshalError(yamlError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode reads one document, so anything after the first "---" would be dropped silently.
|
||||||
|
switch err := decoder.Decode(&yaml.Node{}); {
|
||||||
|
case err == nil:
|
||||||
|
return nil, types.NewUnmarshalError(types.ErrYAMLMultipleDocuments)
|
||||||
|
case err != io.EOF: //nolint:errorlint
|
||||||
|
return nil, types.NewUnmarshalError(yamlError(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldParseErrors []types.FieldParseError
|
var fieldParseErrors []types.FieldParseError
|
||||||
@@ -315,3 +325,37 @@ func (parser ConfigFileParser) ParseYAML(data []byte) (*Config, error) {
|
|||||||
|
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// configYAMLTypeName is the Go type name the yaml package puts in its messages.
|
||||||
|
var configYAMLTypeName = reflect.TypeFor[configYAML]().String()
|
||||||
|
|
||||||
|
// yamlError rewrites yaml decode errors so they name config keys instead of Go types.
|
||||||
|
func yamlError(err error) error {
|
||||||
|
var loadErrors *yaml.LoadErrors
|
||||||
|
if !errors.As(err, &loadErrors) || len(loadErrors.Errors) == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var builder strings.Builder
|
||||||
|
for i, loadError := range loadErrors.Errors {
|
||||||
|
if i > 0 {
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
if loadError.Mark.Line > 0 {
|
||||||
|
fmt.Fprintf(&builder, "line %d: ", loadError.Mark.Line)
|
||||||
|
}
|
||||||
|
builder.WriteString(yamlErrorMessage(loadError.Message))
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// yamlErrorMessage turns "field x not found in type config.configYAML" into "unknown key "x"".
|
||||||
|
func yamlErrorMessage(message string) string {
|
||||||
|
if field, ok := strings.CutPrefix(message, "field "); ok {
|
||||||
|
if name, _, ok := strings.Cut(field, " not found in type "); ok {
|
||||||
|
return fmt.Sprintf("unknown key %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(message, configYAMLTypeName, "the config file")
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ var (
|
|||||||
ErrRemoteFileTooLarge = errors.New("remote file exceeds " + FormatByteSize(RemoteFetchLimit))
|
ErrRemoteFileTooLarge = errors.New("remote file exceeds " + FormatByteSize(RemoteFetchLimit))
|
||||||
|
|
||||||
ErrResponseBodyTooLarge = errors.New("response body exceeds the body limit")
|
ErrResponseBodyTooLarge = errors.New("response body exceeds the body limit")
|
||||||
|
|
||||||
|
ErrYAMLMultipleDocuments = errors.New("expected a single YAML document, but found more than one")
|
||||||
)
|
)
|
||||||
|
|
||||||
type FieldParseError struct {
|
type FieldParseError struct {
|
||||||
@@ -306,6 +308,27 @@ func (e CLIUnexpectedArgsError) Error() string {
|
|||||||
|
|
||||||
// ======================================== Config File ========================================
|
// ======================================== Config File ========================================
|
||||||
|
|
||||||
|
// ConfigFileError names the file a parse error came from, which nested configFile chains lose.
|
||||||
|
type ConfigFileError struct {
|
||||||
|
Path string
|
||||||
|
error error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigFileError(path string, err error) ConfigFileError {
|
||||||
|
if err == nil {
|
||||||
|
err = errNoError
|
||||||
|
}
|
||||||
|
return ConfigFileError{path, err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConfigFileError) Error() string {
|
||||||
|
return e.error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ConfigFileError) Unwrap() error {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigFileReadError struct {
|
type ConfigFileReadError struct {
|
||||||
error error
|
error error
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user