Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: custom type unmarshalling #26

Closed
wneessen opened this issue Dec 8, 2023 · 4 comments
Closed

Feature request: custom type unmarshalling #26

wneessen opened this issue Dec 8, 2023 · 4 comments

Comments

@wneessen
Copy link
Contributor

wneessen commented Dec 8, 2023

First of all, I love fig. It's such a versatile tool, thanks for this!

I was wondering if there would be a posibility to add config parsing/unmarshaling for custom types, like encode/json allows to provide a UnmarshalJSON() method. This would help i. e. with custom "enums" type that are set up via iota.

Here is an example:
Let's say I have a custom type ListenerType and I pre-define 3 types via const:

// ListenerType is an enumeration wrapper for the different listener types
type ListenerType uint

const (
        ListenerUnix ListenerType = iota
        ListenerTCP
        ListenerTLS
)

In my config struct, I would then define my config setting with the corresponding ListenerType and a default value:

// Config holds all the global configuration settings that are parsed by fig
type Config struct {
        // Server holds server specific configuration values
        Server struct {
                PIDFile      string               `fig:"pid_file" default:"/var/run/app.pid"`
                ListenerType ListenerType `fig:"listener_type" default:"unix"`
        }
}

By default this would of course not work, as the default or provided setting in the config file would be a string, not an uint. But here comes my request into play. fig defines an interface like this:

type Unmarshaler interface {
        UnmarshalType(string) error
}

and checks if the corresponding type provides a method that satisfies this interface. For my example it could look like this:

func (l *ListenerType) UnmarshalType(v string) error {
        switch strings.ToLower(v) {
        case "unix":
                *l = ListenerUnix
        case "tcp":
                *l = ListenerTCP
        case "tls":
                *l = ListenerTLS
        default:
                return fmt.Errorf("unknown listener type: %s", v)
        }
        return nil
}

This way, I can keep my iota type but fill the values via fig with the actual string values instead of the user having to provide "0", "1" or "2".

Hope this makes sense and is something you would consider adding.

@kkyr
Copy link
Owner

kkyr commented Dec 9, 2023

Hey, this is a great suggestion! Would make configs look a lot nicer :)

We could add a custom mapstructure DecodeHook and let it do the rest. Here's what I quickly cooked up:

type StringUnmarshaler interface {
	UnmarshalString(s string) error
}

func (l *ListenerType) UnmarshalString(s string) error {
	switch strings.ToLower(s) {
	case "unix":
		*l = ListenerUnix
	...
	}
	return nil
}

func StringToStringUnmarshalerHook() mapstructure.DecodeHookFunc {
	return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
		if f.Kind() != reflect.String {
			return data, nil
		}

		if reflect.PointerTo(t).Implements(reflect.TypeOf((*StringUnmarshaler)(nil)).Elem()) {
			val := reflect.New(t).Interface()

			if unmarshaler, ok := val.(StringUnmarshaler); ok {
				err := unmarshaler.UnmarshalString(data.(string))
				if err != nil {
					return nil, err
				}

				return reflect.ValueOf(val).Elem().Interface(), nil
			}
		}

		return data, nil
	}
}

// during map structure decoder init
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
	Result: &cfg,
	DecodeHook: mapstructure.ComposeDecodeHookFunc(
		StringToStringUnmarshalerHook(),
	),
})

WDYT?

@wneessen
Copy link
Contributor Author

wneessen commented Dec 10, 2023

I tested it on my local fork of fig and it works like a charm. Thanks for the quick turnaround! Looking forward to use this.

@kkyr
Copy link
Owner

kkyr commented Dec 10, 2023

If you'd like to add this please open a PR and I'll gladly review.

@wneessen
Copy link
Contributor Author

PR #29 created

@kkyr kkyr closed this as completed Dec 12, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants