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

feat(unmarshaler): add interface so that children nodes can define custom unmarshal behavior #11

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

pkarakal
Copy link

This adds the Unmarshaler interface where structs have to implement the UnmarshalMapStructure method and then on decoder.Decode, this will trigger the custom unmarshal method. This will make sure that mapstructure is more in line with the rest of the marshaling libraries (like json and yaml)

Copy link
Member

@sagikazarmark sagikazarmark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pkarakal thanks for the PR.

Before accepting it, I'd like to run through a couple questions (see comments) and a potential alternative solution:

Do you think this feature could be implemented as a decode hook?

I think it could be, in which case, it may be a better approach and may reduce complexity.

I've implemented something similar here a while ago: https://github.com/sagikazarmark/mapstructurex/blob/main/decode_hook_map_decoder.go

@@ -301,6 +301,10 @@ type Metadata struct {
Unset []string
}

type Unmarshaler interface {
UnmarshalMapStructure(interface{}) error
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit-picking: can we brainstorm on this name a bit? It doesn't sit right with me right off the bat.

A couple ideas:

  • UnmarshalMap (might be too generic)
  • UnmarshalMapstructure (feels closer to the library name)

@@ -301,6 +301,10 @@ type Metadata struct {
Unset []string
}

type Unmarshaler interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this PR gets accepted as it is, I think it might make sense to document the behavior on the interface.

(Technically, the interface doesn't have to be exported, but it's a good place to document the feature)

return fmt.Errorf("error unmarshaling")
}

func ExampleDecode_structImplementsUnmarshalerInterface() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should add more sophisticated examples. Particularly for maps, named types, pointers, pointers of pointers, etc.

@pkarakal
Copy link
Author

It can be turned into a decode hook for sure. What I mostly had in mind is having an interface that structs could implement to control the way they are decoded into a struct. This can definitely be controlled by a feature flag in the DecoderConfig.

I believe that it would a good feature to have for anyone trying to manipulate a parsed config without having to write a custom decode hook. JSON and YAML packages already have a custom Unmarshaler interfaces for such cases but say for example you want to support multiple config types (json, yaml, toml for example using viper), you'd have to basically implement the same function for each of those configs. Having mapstructure offer such a functionality would mean that one would have to do that just once, drastically simplifying the code.

Take for example the following use case: I have a program that can have multiple data stores (e.g. a database, a message broker) that implement a common interface. To create those data stores, I have a Config interface which has a method that instantiate them and can have different fields. What I want to have is a global config that takes an array of those configs and based on the key I provide parse the decode the element as the correct config type to then instantiate the store. (Basically turn the YAML into the following struct, where the Config name becomes the key)

scheduleStores:
  - storeName: rdb
    postgres:
      - host: "127.0.0.1"
        port: 5432
        user: "root"
        password: "pass"
        name: "postgres"
  - storeName: amqp
    rabbitmq:
      - host: "127.0.0.1"
        port: 5672
        protocol: "amqp"
        vhost: "/
        username: "admin"
        password: "admin"
        exchange: "default
        queue: "default"
type ScheduleStoreConfig struct {
	StoreName      string        `mapstructure:"storeName"`
	ScheduleConfig store.Configs `mapstructure:"-,omitempty"`
}

type Config interface {
	// Name returns the name of the store backend.
	Name() string
        // other methods
}

type PGConfig struct {
	Port         int    `mapstructure:"port"`
	DatabaseHost string `mapstructure:"host"`
	User         string `mapstructure:"user"`
	Password     string `mapstructure:"password"`
	DatabaseName string `mapstructure:"name"`
}

func (c *PGConfig) Name() string {
	return "postgres"
}

type RMQConfig struct {
	Port     uint64 `mapstructure:"port"`
	Protocol string `mapstructure:"protocol"`
	Host     string `mapstructure:"host"`
	VHost    string `mapstructure:"vhost"`
	Username string `mapstructure:"username"`
	Password string `mapstructure:"password"`
	Exchange string `mapstructure:"exchange"`
	Queue    string `mapstructure:"queue"`
}

func (c *RMQConfig) Name() string {
	return "rabbitmq"
}

On the PR comments, no strong feelings on the method name and I can definitely add documentation and more complex test cases. Just trying to see if what I'm describing is something you'd like in the library.

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

Successfully merging this pull request may close these issues.

2 participants