caarlos0 / env

A simple, zero-dependencies library to parse environment variables into structs
https://pkg.go.dev/github.com/caarlos0/env/v11
MIT License
4.91k stars 252 forks source link

Options to mutate value #315

Closed Itsindigo closed 4 months ago

Itsindigo commented 5 months ago

Context

In my application, I've had to base64 encode one of my environment variables because Docker does not support multi-line values in a .env file.

I was wondering if it would be possible to add functionality to mutate environment variables at parse time and potentially return a different type.

I've tried a few different approaches, which I'll outline below.

Custom Parser Using a FuncMap

type B64 string

type AppConfig struct {
    Coinbase CoinbaseConfig
}

type CoinbaseConfig struct {
    ApiKeyName string `env:"CB_API_KEY,required"`
    Secret     B64    `env:"CB_API_PRIVACY_KEY_B64,required"`
}

func customParserOptions() env.Options {
    return env.Options{FuncMap: map[reflect.Type]env.ParserFunc{
        reflect.TypeOf(B64("")): func(v string) (interface{}, error) {
            // do some decode logic
            return B64(v), nil
        },
    }}
}

cfg := AppConfig{}
err := env.ParseWithOptions(&cfg, customParserOptions())

The problem with this approach is that you create a new cfg type when mutating from B64 to string, which is not allowed.

Attempt to Mutate with OnSet Hook

I explored checking the tag for a substring and decoding if there was a match. While I can decode here, the issue is that OnSet does not allow for direct mutation or returning values to be handled elsewhere.

return env.Options{
    OnSet: func(tag string, value interface{}, isDefault bool) {
        if strings.HasPrefix(tag, "B64_") || strings.HasSuffix(tag, "_B64") {
            // Perform decode logic here
        }
    }
}

I tried currying the function by passing pointers to my config variable and an error pointer, but the issue arises once you have decoded, which struct property do you assign the new value to?

This lead me to try passing an map of struct properties to callbacks, but the code quickly became very convoluted and I think we can probably do better.

Suggestion: Would it be possible to pass a pointer to the parsed value that OnSet can mutate, and allow OnSet to return an error that could be bubbled up?

Attempt to Process Field with Custom FieldParams

I looked into doing something like this, but the processFieldFn / FieldParams aren't exposed in the public API:

type CoinbaseConfig struct {
    ApiKeyName string `env:"CB_API_KEY,required"`
    Secret     string `env:"CB_API_PRIVACY_KEY_B64,required,b64"`
}

Suggestion: Could the API be extended to support custom processors?


Let me know your thoughts and whether or not I'm missing something obvious!

If you think any of these suggestions make sense to implement, let me know and I'd be happy to help with the implementation!

Thanks :)

caarlos0 commented 5 months ago

You can do so using text unmarshaller, see example: https://github.com/caarlos0/env/blob/c4db909639710cfd8bd22450759a838d225fce9e/env_test.go#L2018

Itsindigo commented 5 months ago

Awesome, thank you