Skip to content

Commit

Permalink
return custom error from Parse
Browse files Browse the repository at this point in the history
  • Loading branch information
davidnewhall committed Jul 4, 2024
1 parent 8fa7fd7 commit 91d507b
Showing 1 changed file with 59 additions and 27 deletions.
86 changes: 59 additions & 27 deletions filepath.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Opts struct {
MaxSize uint
// Setting NoTrim to true will skip strings.TrimSpace on the read file contents.
NoTrim bool
// Name is prefixed to element names when an error is returned.
// Name is prefixed to element names. You will find the derived name in errors, and in the map output.
// The default name is "Config" if this is omitted.
Name string
output map[string]string
Expand All @@ -33,6 +33,28 @@ const (
DefaultName = "Config"
)

// ParseError is returned when there's an error reading a string-parsed file.
type ParseError struct {
// name of the failed element
Element string
// name of the failed file
FilePath string
// error returned reading the file
Inner error
}

// Error satisfies the standard Go library error interface.
// We do not print the filepath because it's (always?) included in the Inner error.
func (p *ParseError) Error() string {
const prefix = "element failure"
return prefix + ": " + p.Element + ": " + p.Inner.Error()

Check failure on line 50 in filepath.go

View workflow job for this annotation

GitHub Actions / golangci-lint (windows)

return with no blank line before (nlreturn)

Check failure on line 50 in filepath.go

View workflow job for this annotation

GitHub Actions / golangci-lint (darwin)

return with no blank line before (nlreturn)

Check failure on line 50 in filepath.go

View workflow job for this annotation

GitHub Actions / golangci-lint (linux)

return with no blank line before (nlreturn)

Check failure on line 50 in filepath.go

View workflow job for this annotation

GitHub Actions / golangci-lint (freebsd)

return with no blank line before (nlreturn)
}

// Unwrap is used to make the custom error work with errors.Is and errors.As.
func (p *ParseError) Unwrap() error {
return p.Inner // Return the wrapped error.
}

// Parse parses a data structure and searches for strings. It is fully recursive, and will find strings
// in slices, embedded structs, maps and pointers. If the found string has a defined prefix (filepath: by default),
// then the provided filepath is opened, read, and the contents are saved into the string. Replacing the filepath
Expand All @@ -41,12 +63,14 @@ const (
// and it will automatically go to work filling in any extra external config data. Opts may be nil, uses defaults.
// The output map is a map of Config.Item => filepath. Use this to see what files were read-in for each config path.
// If there is an element failure, the failed element and all prior parsed elements will be present in the map.
// Unwrap errors into a ParseError to get the failed file name and a derived name of the element it was found in.
func Parse(input interface{}, opts *Opts) (map[string]string, error) {
data := reflect.TypeOf(input)
if data.Kind() != reflect.Ptr || data.Elem().Kind() != reflect.Struct {
return nil, ErrNotPtr
}

// opts is an optional input, but required in this package.
if opts == nil {
opts = &Opts{
Prefix: DefaultPrefix,
Expand All @@ -55,7 +79,7 @@ func Parse(input interface{}, opts *Opts) (map[string]string, error) {
output: make(map[string]string),
}
} else {
opts = &Opts{ // make a copy for thread safety
opts = &Opts{ // make a copy to make the map thread safe.
Prefix: opts.Prefix,
MaxSize: opts.MaxSize,
NoTrim: opts.NoTrim,
Expand All @@ -64,6 +88,7 @@ func Parse(input interface{}, opts *Opts) (map[string]string, error) {
}
}

// Set defaults for omitted values.
if opts.MaxSize == 0 {
opts.MaxSize = DefaultMaxSize
}
Expand All @@ -76,26 +101,29 @@ func Parse(input interface{}, opts *Opts) (map[string]string, error) {
opts.Name = DefaultName
}

// parse the input struct and return the outmap.
return opts.output, opts.parseStruct(reflect.ValueOf(input).Elem(), opts.Name)
}

func (o *Opts) parseStruct(field reflect.Value, name string) error {
for i := field.NumField() - 1; i >= 0; i-- {
name := name + "." + field.Type().Field(i).Name // name is overloaded here.
if err := o.parseElement(field.Field(i), name); err != nil {
// If you pass in a non-struct to this function, you'll experience a panic.
func (o *Opts) parseStruct(elem reflect.Value, name string) error {
for i := elem.NumField() - 1; i >= 0; i-- {
name := name + "." + elem.Type().Field(i).Name // name is overloaded here.
if err := o.parseElement(elem.Field(i), name); err != nil {
return err
}
}

return nil
}

func (o *Opts) parseMap(field reflect.Value, name string) error {
for _, key := range field.MapKeys() {
// If you pass in a non-map to this function, you'll experience a panic.
func (o *Opts) parseMap(elem reflect.Value, name string) error {
for _, key := range elem.MapKeys() {
// Copy the map field.
fieldCopy := reflect.Indirect(reflect.New(field.MapIndex(key).Type()))
fieldCopy := reflect.Indirect(reflect.New(elem.MapIndex(key).Type()))
// Set the copy's value to the value of the original.
fieldCopy.Set(field.MapIndex(key))
fieldCopy.Set(elem.MapIndex(key))

// Parse the copy, because map values cannot be .Set() directly.
name := fmt.Sprint(name, "[", key, "]") // name is overloaded here.
Expand All @@ -104,16 +132,16 @@ func (o *Opts) parseMap(field reflect.Value, name string) error {
}

// Update the map index with the possibly-modified copy that got parsed.
field.SetMapIndex(key, fieldCopy)
elem.SetMapIndex(key, fieldCopy)
}

return nil
}

func (o *Opts) parseSlice(field reflect.Value, name string) error {
for idx := field.Len() - 1; idx >= 0; idx-- {
name := fmt.Sprint(name, "[", idx+1, "/", field.Len(), "]") // name is overloaded here.
if err := o.parseElement(field.Index(idx), name); err != nil {
func (o *Opts) parseSlice(slice reflect.Value, name string) error {
for idx := slice.Len() - 1; idx >= 0; idx-- {
name := fmt.Sprint(name, "[", idx+1, "/", slice.Len(), "]") // name is overloaded here.
if err := o.parseElement(slice.Index(idx), name); err != nil {
return err
}
}
Expand All @@ -122,25 +150,25 @@ func (o *Opts) parseSlice(field reflect.Value, name string) error {
}

// parseElement processes any supported element type.
func (o *Opts) parseElement(field reflect.Value, name string) error {
switch kind := field.Kind(); kind { //nolint:exhaustive
func (o *Opts) parseElement(elem reflect.Value, name string) error {
switch kind := elem.Kind(); kind { //nolint:exhaustive
case reflect.String:
return o.parseString(field, name)
return o.parseString(elem, name)
case reflect.Struct:
return o.parseStruct(field, name)
return o.parseStruct(elem, name)
case reflect.Pointer, reflect.Interface:
return o.parseElement(field.Elem(), name)
return o.parseElement(elem.Elem(), name)
case reflect.Slice, reflect.Array:
return o.parseSlice(field, name)
return o.parseSlice(elem, name)
case reflect.Map:
return o.parseMap(field, name)
return o.parseMap(elem, name)
default:
return nil
}
}

func (o *Opts) parseString(field reflect.Value, name string) error {
value := field.String()
func (o *Opts) parseString(elem reflect.Value, name string) error {
value := elem.String()
if !strings.HasPrefix(value, o.Prefix) {
return nil
}
Expand All @@ -150,13 +178,17 @@ func (o *Opts) parseString(field reflect.Value, name string) error {

data, err := readFile(o.output[name], o.MaxSize)
if err != nil {
return fmt.Errorf("element failure: %s: %w", name, err)
return &ParseError{
Element: name,
FilePath: o.output[name],
Inner: err,
}
}

if o.NoTrim {
field.SetString(data)
elem.SetString(data)
} else {
field.SetString(strings.TrimSpace(data))
elem.SetString(strings.TrimSpace(data))
}

return nil
Expand Down

0 comments on commit 91d507b

Please sign in to comment.