Skip to content

Add MapstructureUnmarshaler interface to allow types to decide how to be unmarshalled #35

@chancez

Description

@chancez

In cilium/cilium we have a few custom cell.DecodeHooks for unmarshalling values into custom types: https://github.com/cilium/cilium/blob/main/pkg/hive/hive.go#L105-L132

Unfortunately, this requires centralizing the configuration of how to handle the custom types and they run after the default hooks: https://github.com/cilium/hive/blob/main/cell/config.go#L126

I propose we add an interface to give types the ability to define their own method of unmarshalling:

type MapstructureUnmarshaler interface {
	UnmarshalMapstructure(any) error
}

Then, we could define a new mapstructure.DecodeHookFunc:

func MapstructureUnmarshalerHook() mapstructure.DecodeHookFunc {
	// Loosely adapted from the json.Unmarshal implementation.
	// Specifically, the indirect() function:
	// https://cs.opensource.google/go/go/+/refs/tags/go1.21.6:src/encoding/json/decode.go;l=426
	return func(from, to reflect.Value) (interface{}, error) {
		// If v is a named type and is addressable,
		// start with its address, so that if the type has pointer methods,
		// we find them.
		if to.Kind() != reflect.Pointer && to.CanAddr() {
			to = to.Addr()
		}

		// Still not a pointer, so no point in continuing
		if to.Kind() != reflect.Pointer {
			return from.Interface(), nil
		}

		// In order for the UnmarshalMapstructure to assign to 'to', then it needs
		// to be non-nil
		if to.IsNil() {
			to.Set(reflect.New(to.Type().Elem()))
		}

		// convert to the interface, if it's not the desired interface, return the
		// value unchanged
		u, ok := to.Interface().(MapstructureUnmarshaler)
		if !ok {
			return from.Interface(), nil
		}

		// unmarshal "from" into 'to' using
		if err := u.UnmarshalMapstructure(from.Interface()); err != nil {
			return nil, err
		}

		return u, nil
	}
}

This would enable removing the hooks here: https://github.com/cilium/cilium/blob/main/pkg/hive/hive.go#L105-L132 and instead the logic could be implemented as methods on the types themselves. In the case of protobuf types, it would require a wrapper type to implement the method on, but I think that's acceptable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions