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

Added support converting unmarshalled configs to interface{} #460

Merged
merged 8 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Support converting unmarshalled configurations to `interface{}`.

## [1.3.0] - 2024-08-26

### Removed
Expand Down
6 changes: 5 additions & 1 deletion internal/convert/converter.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c Converter) Convert(from, to any) error {
return c.convert("", from, toVal)
}

func (c Converter) convert(name string, from any, toVal reflect.Value) error { //nolint:cyclop
func (c Converter) convert(name string, from any, toVal reflect.Value) error { //nolint:cyclop,funlen
if from == nil {
return nil // Do nothing if from is nil.
}
Expand Down Expand Up @@ -91,6 +91,10 @@ func (c Converter) convert(name string, from any, toVal reflect.Value) error { /
return c.convertString(name, fromVal, toVal)
case toVal.Kind() == reflect.Struct:
return c.convertStruct(name, fromVal, toVal)
case toVal.Kind() == reflect.Interface: // Right after all other checks.
toVal.Set(fromVal)
Copy link
Collaborator

Choose a reason for hiding this comment

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

It needs a special handling if fromVal is map since the keys in map may be wrapped for case sensitive.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't get your point. Special handling is required for maps, asaik, but it is the caller's responsibility to deal with keys and values if the destination contains fields of type interface{}.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sorry for confusion. To support case sensitive map key, the key in internal value has been wrapped with a special struct (https://github.com/nil-go/konf/blob/main/config.go#L166). So when unmarshaling back to map, it needs to unwrap the key back to string (case sensitive or not depends on configuration).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, there's no confusion. I just had doubts about whether special handling was really necessary here. I found a test case that clearly shows it is. =)


return nil
default:
// If it reached here then it weren't able to convert it.
return fmt.Errorf("%s: unsupported type: %s", name, toVal.Kind()) //nolint:err113
Expand Down
117 changes: 98 additions & 19 deletions internal/convert/converter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"github.com/nil-go/konf/internal/assert"
"github.com/nil-go/konf/internal/convert"
"github.com/nil-go/konf/internal/maps"
)

func TestConverter(t *testing.T) { //nolint:maintidx
Expand Down Expand Up @@ -835,34 +836,39 @@ func TestConverter(t *testing.T) { //nolint:maintidx
}),
},
from: map[string]any{
"Enum": "sky",
"OuterField": "outer",
"PrivateField": "private",
"InnerField": "squash",
"Inner": map[string]any{"InnerField": "inner"},
"Enum": "sky",
"OuterField": "outer",
"PrivateField": "private",
"InterfaceField": "interface{}",
"InnerField": "squash",
"Inner": map[string]any{"InnerField": "inner"},
},
to: pointer(OuterStruct{}),
expected: pointer(OuterStruct{
Enum: Sky,
OuterField: "outer",
InnerStruct: InnerStruct{InnerField: "squash"},
Inner: &InnerStruct{InnerField: "inner"},
Enum: Sky,
OuterField: "outer",
InterfaceField: "interface{}",
InnerStruct: InnerStruct{InnerField: "squash"},
Inner: &InnerStruct{InnerField: "inner"},
}),
},
{
description: "map to struct (with keyMap)",
opts: []convert.Option{
convert.WithKeyMapper(strings.ToLower),
},
from: map[string]string{"innerfield": "inner"},
from: map[string]string{"innerfield": "inner", "interfacefield": "interface{}"},
to: pointer(struct {
InnerField string
InnerField string
InterfaceField interface{}
}{}),
expected: pointer(
struct {
InnerField string
InnerField string
InterfaceField interface{}
}{
InnerField: "inner",
InnerField: "inner",
InterfaceField: "interface{}",
}),
},
{
Expand Down Expand Up @@ -896,13 +902,85 @@ func TestConverter(t *testing.T) { //nolint:maintidx
to: pointer(OuterStruct{}),
err: "'' expected a map, got 'string'",
},
// unsupported.
{
description: "to interface (unsupported)",
description: "int to interface",
from: 42,
to: pointer(any(nil)),
expected: pointer(any(42)),
},
{
description: "string to interface",
from: "str",
to: pointer(any(nil)),
err: ": unsupported type: interface",
expected: pointer(any("str")),
},
{
description: "float to interface",
from: 42.42,
to: pointer(any(nil)),
expected: pointer(any(42.42)),
},
{
description: "map to interface",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added few more tests to check if maps.Unpack call is required. Seems to be excessive.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I may miss test for Unpack in convert package. Here is the code calls maps.Unpack

key, value := maps.Unpack(fromValueVal.Interface())

Copy link
Collaborator

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I may miss test for Unpack in convert package. Here is the code calls maps.Unpack

key, value := maps.Unpack(fromValueVal.Interface())

Seems to be tested indirectly.

from: map[string]int{"key": 42, "keySensitive": 43},
to: pointer(any(nil)),
expected: pointer(any(map[string]int{"key": 42, "keySensitive": 43})),
},
{
description: "map to interface (with keyMap)", // Probably redundant.
opts: []convert.Option{
convert.WithKeyMapper(strings.ToLower),
},
from: map[string]int{"key": 42, "keysensitive": 43},
to: pointer(any(nil)),
expected: pointer(any(map[string]int{"key": 42, "keysensitive": 43})),
},
{
description: "packed KV and field to map[string]interface{}",
from: map[string]interface{}{
"key1": maps.KeyValue{
Key: "key1",
Value: "value1",
},
"key2": "value2",
},
to: pointer(map[string]interface{}{}),
expected: pointer(map[string]interface{}{
"key1": "value1",
"key2": "value2",
}),
},
{
description: "packed KV and field to struct (with keyMap)",
opts: []convert.Option{
convert.WithKeyMapper(strings.ToLower),
},
from: map[string]interface{}{
"key1": maps.KeyValue{
Key: "key1",
Value: "value1",
},
"key2": "value2",
},
to: pointer(struct {
Key1 interface{}
Key2 interface{}
}{}),
expected: pointer(struct {
Key1 interface{}
Key2 interface{}
}{
Key1: "value1",
Key2: "value2",
}),
},
{
description: "slice to interface",
from: []int{1, 2, 3},
to: pointer(any(nil)),
expected: pointer(any([]int{1, 2, 3})),
},
// unsupported.
{
description: "to func (unsupported)",
from: "str",
Expand Down Expand Up @@ -966,9 +1044,10 @@ func (e *Enum) UnmarshalText(text []byte) error {

type (
OuterStruct struct {
Enum Enum
OuterField string
privateField string //nolint:unused
Enum Enum
OuterField string
privateField string //nolint:unused
InterfaceField interface{}

InnerStruct `konf:",squash"`
Inner *InnerStruct
Expand Down
Loading