Skip to content

Commit

Permalink
Add ErrorUnset option to DecoderConfig and Unset array to Metddata
Browse files Browse the repository at this point in the history
This commit extends the DecoderConfig with an option to fail decoding
if there exist fields in the target struct, which haven't been set
during decoding due to a missing, corresponding value in the input map.

Accordingly, the Metadata has been extended with an Unset array to
receive all field names which have not been set during decoding.
  • Loading branch information
SaschaRoland committed Dec 18, 2020
1 parent 95075d6 commit d91ccce
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 1 deletion.
37 changes: 36 additions & 1 deletion mapstructure.go
Expand Up @@ -200,6 +200,11 @@ type DecoderConfig struct {
// (extra keys).
ErrorUnused bool

// If ErrorUnset is true, then it is an error for there to exist
// fields in the result that were not set in the decoding process
// (extra fields).
ErrorUnset bool

// ZeroFields, if set to true, will zero fields before writing them.
// For example, a map will be emptied before decoded values are put in
// it. If this is false, a map will be merged.
Expand Down Expand Up @@ -264,6 +269,11 @@ type Metadata struct {
// Unused is a slice of keys that were found in the raw value but
// weren't decoded since there was no matching field in the result interface
Unused []string

// Unset is a slice of field names that were found in the result interface
// but weren't set in the decoding process since there was no matching value
// in the input
Unset []string
}

// Decode takes an input structure and uses reflection to translate it to
Expand Down Expand Up @@ -355,6 +365,10 @@ func NewDecoder(config *DecoderConfig) (*Decoder, error) {
if config.Metadata.Unused == nil {
config.Metadata.Unused = make([]string, 0)
}

if config.Metadata.Unset == nil {
config.Metadata.Unset = make([]string, 0)
}
}

if config.TagName == "" {
Expand Down Expand Up @@ -1225,6 +1239,7 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
dataValKeysUnused[dataValKey.Interface()] = struct{}{}
}

targetValKeysUnused := make(map[interface{}]struct{})
errors := make([]string, 0)

// This slice will keep track of all the structs we'll be decoding.
Expand Down Expand Up @@ -1329,7 +1344,8 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e

if !rawMapVal.IsValid() {
// There was no matching key in the map for the value in
// the struct. Just ignore.
// the struct. Remember it for potential errors and metadata.
targetValKeysUnused[fieldName] = struct{}{}
continue
}
}
Expand Down Expand Up @@ -1389,6 +1405,17 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e
errors = appendErrors(errors, err)
}

if d.config.ErrorUnset && len(targetValKeysUnused) > 0 {
keys := make([]string, 0, len(targetValKeysUnused))
for rawKey := range targetValKeysUnused {
keys = append(keys, rawKey.(string))
}
sort.Strings(keys)

err := fmt.Errorf("'%s' has unset fields: %s", name, strings.Join(keys, ", "))
errors = appendErrors(errors, err)
}

if len(errors) > 0 {
return &Error{errors}
}
Expand All @@ -1403,6 +1430,14 @@ func (d *Decoder) decodeStructFromMap(name string, dataVal, val reflect.Value) e

d.config.Metadata.Unused = append(d.config.Metadata.Unused, key)
}
for rawKey := range targetValKeysUnused {
key := rawKey.(string)
if name != "" {
key = name + "." + key
}

d.config.Metadata.Unset = append(d.config.Metadata.Unset, key)
}
}

return nil
Expand Down
32 changes: 32 additions & 0 deletions mapstructure_test.go
Expand Up @@ -1225,6 +1225,30 @@ func TestDecoder_ErrorUnused_NotSetable(t *testing.T) {
t.Fatal("expected error")
}
}
func TestDecoder_ErrorUnset(t *testing.T) {
t.Parallel()

input := map[string]interface{}{
"vstring": "hello",
"foo": "bar",
}

var result Basic
config := &DecoderConfig{
ErrorUnset: true,
Result: &result,
}

decoder, err := NewDecoder(config)
if err != nil {
t.Fatalf("err: %s", err)
}

err = decoder.Decode(input)
if err == nil {
t.Fatal("expected error")
}
}

func TestMap(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -2192,6 +2216,14 @@ func TestMetadata(t *testing.T) {
if !reflect.DeepEqual(md.Unused, expectedUnused) {
t.Fatalf("bad unused: %#v", md.Unused)
}

expectedUnset := []string{
"Vbar.Vbool", "Vbar.Vdata", "Vbar.Vextra", "Vbar.Vfloat", "Vbar.Vint",
"Vbar.VjsonFloat", "Vbar.VjsonInt", "Vbar.VjsonNumber"}
sort.Strings(md.Unset)
if !reflect.DeepEqual(md.Unset, expectedUnset) {
t.Fatalf("bad unset: %#v", md.Unset)
}
}

func TestMetadata_Embedded(t *testing.T) {
Expand Down

0 comments on commit d91ccce

Please sign in to comment.