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

Make features.BindStruct a Variable or an Option, Instead of Build Constraints #1851

Closed
1 task done
motoki317 opened this issue Jun 4, 2024 · 8 comments · Fixed by #1854
Closed
1 task done

Make features.BindStruct a Variable or an Option, Instead of Build Constraints #1851

motoki317 opened this issue Jun 4, 2024 · 8 comments · Fixed by #1854
Labels
kind/enhancement New feature or request

Comments

@motoki317
Copy link

motoki317 commented Jun 4, 2024

Preflight Checklist

  • I have searched the issue tracker for an issue that matches the one I want to file, without success.

Problem Description

The features.BindStruct feature flag (which is also the only one feature flag in this package) is currently implemented as a build constraint here:

//go:build viper_bind_struct
package features
const BindStruct = true

Being implemented as a build constraint, users have to add a whole new viper_bind_struct build tag to their build scripts, in order to enable this feature.
This was quite surprising (to me, at least), and I think it's making harder for people to use this feature.

Proposed Solution

Option 1: Make this feature flag a dynamically configurable variable instead of a const regulated by build constraints, and move the features package outside of the internal package.

package features

var BindStruct = false

This way, users of viper can dynamically enable this feature from normal golang source code, like as follows:

import "github.com/spf13/viper/features" // Just an example new package path

func main() {
    features.BindStruct = true // Enable the feature

    // Use the feature...
    var c MyConfig
    viper.AutomaticEnv()
    viper.Unmarshal(&c)
}

Option 2: Make this to an option that you can pass to viper.NewWithOptions, or something that you can configure later with viper instances.
I believe this was the intention of this original TODO comment here:

viper/viper.go

Line 979 in 272344e

// TODO: make this optional?

func main() {
    v := viper.NewWithOptions(viper.BindStruct()) // Enable the feature (just an example option name)

    // Use the feature...
    var c MyConfig
    v.AutomaticEnv()
    v.Unmarshal(&c)
}

or something similar to AutomaticEnv() usage:

func main() {
    var c MyConfig
    viper.BindStruct() // Enable the feature (just an example option name)
    viper.AutomaticEnv()
    viper.Unmarshal(&c)

    // or to a specific viper instance
    v := viper.New()
    v.BindStruct()
    v.AutomaticEnv()
    v.Unmarshal(&c)
}

Alternatives Considered

No response

Additional Information

Related issues and PRs:

@motoki317 motoki317 added the kind/enhancement New feature or request label Jun 4, 2024
Copy link

github-actions bot commented Jun 4, 2024

👋 Thanks for reporting!

A maintainer will take a look at your issue shortly. 👀

In the meantime: We are working on Viper v2 and we would love to hear your thoughts about what you like or don't like about Viper, so we can improve or fix those issues.

⏰ If you have a couple minutes, please take some time and share your thoughts: https://forms.gle/R6faU74qPRPAzchZ9

📣 If you've already given us your feedback, you can still help by spreading the news,
either by sharing the above link or telling people about this on Twitter:

https://twitter.com/sagikazarmark/status/1306904078967074816

Thank you! ❤️

@sagikazarmark
Copy link
Collaborator

How do you feel about #1854?

I'd imagine it would become the default behavior at some point, so the experimental flag would become noop.

@motoki317
Copy link
Author

How do you feel about #1854?

I'd imagine it would become the default behavior at some point, so the experimental flag would become noop.

Thank you for reviewing this quickly!

I looked at the PR changes, it looks mostly OK, but I guess the build tag will remain to control the behavior of the global viper instance?

I think it's best not to let users depend on custom build tags, even if they're experimental.
Exposing these options as ExperimentalFoo() functions or options would be much easier to use than a build tag.
In case the experimental options go away, it would break the builds for clients using the function so users can easily notice the breaking change.

So I think it would be more natural if the global and custom viper instances had similar ways to configure the behavior.
This would also be similar with many existing options, such as:

viper/viper.go

Lines 1404 to 1410 in 3266e43

// AutomaticEnv makes Viper check if environment variables match any of the existing keys
// (config, default or flags). If matching env vars are found, they are loaded into Viper.
func AutomaticEnv() { v.AutomaticEnv() }
func (v *Viper) AutomaticEnv() {
v.automaticEnvApplied = true
}

so how about:

func main() {
    // Enable for the global instance
    viper.ExperimentalBindStruct()
    // Enable for an instance
    v := viper.New()
    v.ExperimentalBindStruct()
}

Perhaps we can change all existing option names into something along WithFoo() in viper v2, so that the API looks more tidy and users won't be intimidated when they type viper. with autocompletes.

@sagikazarmark
Copy link
Collaborator

The build tag controls the default behavior. Incidentally, you are right: it also controls the global Viper behavior.

I try to discourage everyone from using the global Viper, though. I don't think it's good practice to use the global instance.

I'm also eliminating the state modification pattern: all new options should be passed through NewWithOptions instead of calling setters. I realize that's a challenge with the global instance. I haven't figured out a good solution there yet.

Let me think about that.

Is the global instance something you use? If so, why? Would it be difficult to migrate to a non-global instance?

@motoki317
Copy link
Author

Yes, I do use global instance of viper to build some applications.
It's mainly because I only need a single instance in a whole application, and I think it's the same for most applications.
It might not be too difficult to migrate to a non-global instance, but sometimes it's just easier to call the package methods directly.
https://github.com/spf13/viper?tab=readme-ov-file#viper-or-vipers

I'm also eliminating the state modification pattern: all new options should be passed through NewWithOptions instead of calling setters. I realize that's a challenge with the global instance. I haven't figured out a good solution there yet.

I agree on that, but removing the current package-level methods and global instance would be too much of a change. Perhaps we can achieve that in v2.

@sagikazarmark
Copy link
Collaborator

Yes, that's a goal for v2.

What do you think about #1856 ?

@motoki317
Copy link
Author

Yes, that's a goal for v2.

What do you think about #1856 ?

I see, I think it's looking good as an option for configuring the global instance for v1.

@andig
Copy link
Contributor

andig commented Jun 7, 2024

Copied from #1854 (comment):

Love it- much simpler to use than the compile switches. Doesn't this also replace #1715? Wondering why that's not visible in the diff? However, it's not possible to use with the global viper instance, right?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind/enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants