diff --git a/core/conf/config_test.go b/core/conf/config_test.go index 82dc5545..7be4f540 100644 --- a/core/conf/config_test.go +++ b/core/conf/config_test.go @@ -97,6 +97,30 @@ d = "abcd!@#$112" assert.Equal(t, "abcd!@#$112", val.D) } +func TestConfigOptional(t *testing.T) { + text := `a = "foo" +b = 1 +c = "FOO" +d = "abcd" +` + tmpfile, err := createTempFile(".toml", text) + assert.Nil(t, err) + defer os.Remove(tmpfile) + + var val struct { + A string `json:"a"` + B int `json:"b,optional"` + C string `json:"c,optional=B"` + D string `json:"d,optional=b"` + } + if assert.NoError(t, Load(tmpfile, &val)) { + assert.Equal(t, "foo", val.A) + assert.Equal(t, 1, val.B) + assert.Equal(t, "FOO", val.C) + assert.Equal(t, "abcd", val.D) + } +} + func TestConfigJsonCanonical(t *testing.T) { text := []byte(`{"a": "foo", "B": "bar"}`) diff --git a/core/mapping/jsonunmarshaler_test.go b/core/mapping/jsonunmarshaler_test.go index 05af99c9..832ed24e 100644 --- a/core/mapping/jsonunmarshaler_test.go +++ b/core/mapping/jsonunmarshaler_test.go @@ -930,3 +930,13 @@ func TestUnmarshalJsonArray(t *testing.T) { assert.Equal(t, "kevin", v[0].Name) assert.Equal(t, 18, v[0].Age) } + +func TestUnmarshalJsonBytesError(t *testing.T) { + var v []struct { + Name string `json:"name"` + Age int `json:"age"` + } + + assert.Error(t, UnmarshalJsonBytes([]byte((``)), &v)) + assert.Error(t, UnmarshalJsonReader(strings.NewReader(``), &v)) +} diff --git a/core/mapping/unmarshaler.go b/core/mapping/unmarshaler.go index e4293714..fee1457a 100644 --- a/core/mapping/unmarshaler.go +++ b/core/mapping/unmarshaler.go @@ -372,6 +372,26 @@ func (u *Unmarshaler) parseOptionsWithContext(field reflect.StructField, m Value return key, nil, nil } + if u.opts.canonicalKey != nil { + key = u.opts.canonicalKey(key) + + if len(options.OptionalDep) > 0 { + // need to create a new fieldOption, because the original one is shared through cache. + options = &fieldOptions{ + fieldOptionsWithContext: fieldOptionsWithContext{ + Inherit: options.Inherit, + FromString: options.FromString, + Optional: options.Optional, + Options: options.Options, + Default: options.Default, + EnvVar: options.EnvVar, + Range: options.Range, + }, + OptionalDep: u.opts.canonicalKey(options.OptionalDep), + } + } + } + optsWithContext, err := options.toOptionsWithContext(key, m, fullName) if err != nil { return "", nil, err diff --git a/core/mapping/unmarshaler_test.go b/core/mapping/unmarshaler_test.go index fc4d2cd4..ca57cac5 100644 --- a/core/mapping/unmarshaler_test.go +++ b/core/mapping/unmarshaler_test.go @@ -77,6 +77,26 @@ func TestUnmarshalWithoutTagNameWithCanonicalKey(t *testing.T) { } } +func TestUnmarshalWithoutTagNameWithCanonicalKeyOptionalDep(t *testing.T) { + type inner struct { + FirstName string `key:",optional"` + LastName string `key:",optional=FirstName"` + } + m := map[string]interface{}{ + "firstname": "go", + "lastname": "zero", + } + + var in inner + unmarshaler := NewUnmarshaler(defaultKeyName, WithCanonicalKeyFunc(func(s string) string { + return strings.ToLower(s) + })) + if assert.NoError(t, unmarshaler.Unmarshal(m, &in)) { + assert.Equal(t, "go", in.FirstName) + assert.Equal(t, "zero", in.LastName) + } +} + func TestUnmarshalBool(t *testing.T) { type inner struct { True bool `key:"yes"` @@ -1099,6 +1119,66 @@ func TestUnmarshalStructOptionalDependsNotEnoughValue(t *testing.T) { assert.Error(t, UnmarshalKey(m, &in)) } +func TestUnmarshalStructOptionalDependsMoreValues(t *testing.T) { + type address struct { + Optional string `key:",optional"` + OptionalDepends string `key:",optional=a=b"` + } + type inner struct { + Name string `key:"name"` + Address address `key:"address"` + } + + m := map[string]interface{}{ + "name": "kevin", + "address": map[string]interface{}{}, + } + + var in inner + assert.Error(t, UnmarshalKey(m, &in)) +} + +func TestUnmarshalStructMissing(t *testing.T) { + type address struct { + Optional string `key:",optional"` + OptionalDepends string `key:",optional=a=b"` + } + type inner struct { + Name string `key:"name"` + Address address `key:"address"` + } + + m := map[string]interface{}{ + "name": "kevin", + } + + var in inner + assert.Error(t, UnmarshalKey(m, &in)) +} + +func TestUnmarshalNestedStructMissing(t *testing.T) { + type mostInner struct { + Name string `key:"name"` + } + type address struct { + Optional string `key:",optional"` + OptionalDepends string `key:",optional=a=b"` + MostInner mostInner + } + type inner struct { + Name string `key:"name"` + Address address `key:"address"` + } + + m := map[string]interface{}{ + "name": "kevin", + "address": map[string]interface{}{}, + } + + var in inner + assert.Error(t, UnmarshalKey(m, &in)) +} + func TestUnmarshalAnonymousStructOptionalDepends(t *testing.T) { type AnonAddress struct { City string `key:"city"` @@ -1422,6 +1502,18 @@ func TestUnmarshalOptionsOptionalWrongValue(t *testing.T) { assert.Error(t, UnmarshalKey(m, &in)) } +func TestUnmarshalOptionsMissingValues(t *testing.T) { + type inner struct { + Value string `key:"value,options"` + } + m := map[string]interface{}{ + "value": "first", + } + + var in inner + assert.Error(t, UnmarshalKey(m, &in)) +} + func TestUnmarshalStringOptionsWithStringOptionsNotString(t *testing.T) { type inner struct { Value string `key:"value,options=first|second"`