From 5f8b2e5c7f9f60fd294000cfcff89b706cc3b57b Mon Sep 17 00:00:00 2001 From: KUOKA Yusuke Date: Fri, 29 Mar 2019 12:38:16 +0900 Subject: [PATCH] feat: Enhanced `get` template function (#513) `get` is now able to take one more optional argument, that is used as the default value when the value for the key does not exist. Resolves #465 Fixes #427 Fixes #357 Ref #460 --- tmpl/context_tmpl_test.go | 19 +++++++++++++--- tmpl/get_or_nil.go | 47 ++++++++++++++++++++++++++++++++------- tmpl/get_or_nil_test.go | 37 ++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 11 deletions(-) diff --git a/tmpl/context_tmpl_test.go b/tmpl/context_tmpl_test.go index d3b08515..e8888c3d 100644 --- a/tmpl/context_tmpl_test.go +++ b/tmpl/context_tmpl_test.go @@ -110,11 +110,11 @@ func TestRenderTemplate_Defaulting(t *testing.T) { } } -func renderTemplateToString(s string) (string, error) { +func renderTemplateToString(s string, data ...interface{}) (string, error) { ctx := &Context{readFile: func(filename string) ([]byte, error) { return nil, fmt.Errorf("unexpected call to readFile: filename=%s", filename) }} - tplString, err := ctx.RenderTemplateToBuffer(s) + tplString, err := ctx.RenderTemplateToBuffer(s, data...) if err != nil { return "", err } @@ -125,6 +125,7 @@ func Test_renderTemplateToString(t *testing.T) { type args struct { s string envs map[string]string + data interface{} } tests := []struct { name string @@ -177,6 +178,18 @@ func Test_renderTemplateToString(t *testing.T) { want: "7", wantErr: false, }, + { + name: "get", + args: args{ + s: `{{ . | get "Foo" }}, {{ . | get "Bar" "2" }}`, + envs: map[string]string{}, + data: map[string]interface{}{ + "Foo": "1", + }, + }, + want: "1, 2", + wantErr: false, + }, { name: "env var not set", args: args{ @@ -225,7 +238,7 @@ func Test_renderTemplateToString(t *testing.T) { t.Error("renderTemplateToString() could not set env var for testing") } } - got, err := renderTemplateToString(tt.args.s) + got, err := renderTemplateToString(tt.args.s, tt.args.data) if (err != nil) != tt.wantErr { t.Errorf("renderTemplateToString() for %s error = %v, wantErr %v", tt.name, err, tt.wantErr) return diff --git a/tmpl/get_or_nil.go b/tmpl/get_or_nil.go index cc514274..72d9b5e1 100644 --- a/tmpl/get_or_nil.go +++ b/tmpl/get_or_nil.go @@ -14,36 +14,67 @@ func (e *noValueError) Error() string { return e.msg } -func get(path string, obj interface{}) (interface{}, error) { +func get(path string, varArgs ...interface{}) (interface{}, error) { + var defSet bool + var def interface{} + var obj interface{} + switch len(varArgs) { + case 1: + defSet = false + def = nil + obj = varArgs[0] + case 2: + defSet = true + def = varArgs[0] + obj = varArgs[1] + default: + return nil, fmt.Errorf("unexpected number of args pased to the template function get(path, [def, ]obj): expected 1 or 2, got %d, args was %v", len(varArgs), varArgs) + } + if path == "" { return obj, nil } keys := strings.Split(path, ".") + var v interface{} + var ok bool switch typedObj := obj.(type) { case map[string]interface{}: - v, ok := typedObj[keys[0]] + v, ok = typedObj[keys[0]] if !ok { + if defSet { + return def, nil + } return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)} } - return get(strings.Join(keys[1:], "."), v) case map[interface{}]interface{}: - v, ok := typedObj[keys[0]] + v, ok = typedObj[keys[0]] if !ok { + if defSet { + return def, nil + } return nil, &noValueError{fmt.Sprintf("no value exist for key \"%s\" in %v", keys[0], typedObj)} } - return get(strings.Join(keys[1:], "."), v) default: maybeStruct := reflect.ValueOf(typedObj) - if maybeStruct.NumField() < 1 { + if maybeStruct.Kind() != reflect.Struct { return nil, &noValueError{fmt.Sprintf("unexpected type(%v) of value for key \"%s\": it must be either map[string]interface{} or any struct", reflect.TypeOf(obj), keys[0])} + } else if maybeStruct.NumField() < 1 { + return nil, &noValueError{fmt.Sprintf("no accessible struct fields for key \"%s\"", keys[0])} } f := maybeStruct.FieldByName(keys[0]) if !f.IsValid() { + if defSet { + return def, nil + } return nil, &noValueError{fmt.Sprintf("no field named \"%s\" exist in %v", keys[0], typedObj)} } - v := f.Interface() - return get(strings.Join(keys[1:], "."), v) + v = f.Interface() } + + if defSet { + return get(strings.Join(keys[1:], "."), def, v) + } + return get(strings.Join(keys[1:], "."), v) } func getOrNil(path string, o interface{}) (interface{}, error) { diff --git a/tmpl/get_or_nil_test.go b/tmpl/get_or_nil_test.go index 2d92c84c..e3eb100d 100644 --- a/tmpl/get_or_nil_test.go +++ b/tmpl/get_or_nil_test.go @@ -4,6 +4,9 @@ import ( "testing" ) +type EmptyStruct struct { +} + func TestGetStruct(t *testing.T) { type Foo struct{ Bar string } @@ -23,6 +26,12 @@ func TestGetStruct(t *testing.T) { if err == nil { t.Errorf("expected error but was not occurred") } + + _, err = get("foo", EmptyStruct{}) + + if err == nil { + t.Errorf("expected error but was not occurred") + } } func TestGetMap(t *testing.T) { @@ -44,6 +53,34 @@ func TestGetMap(t *testing.T) { } } +func TestGet_Default(t *testing.T) { + obj := map[string]interface{}{"Foo": map[string]interface{}{}, "foo": 1} + + v1, err := get("Foo.Bar", "Bar", obj) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if v1 != "Bar" { + t.Errorf("unexpected value for path Foo.Bar in %v: expected=Bar, actual=%v", obj, v1) + } + + v2, err := get("Baz", "Baz", obj) + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if v2 != "Baz" { + t.Errorf("unexpected value for path Baz in %v: expected=Baz, actual=%v", obj, v2) + } + + _, err = get("foo.Bar", "fooBar", obj) + + if err == nil { + t.Errorf("expected error but was not occurred") + } +} + func TestGetOrNilStruct(t *testing.T) { type Foo struct{ Bar string }