From c8449d4ed5fab12bf4b70e2cfb4d062f41051435 Mon Sep 17 00:00:00 2001 From: Judson Date: Tue, 30 Jun 2020 09:28:38 -0700 Subject: [PATCH 1/3] Add dig for dicts Adds a pipeline-compatible `dig` function - traverses a list of keys to return a value, or a default if no value provided. Future work, if required: allow `dig` to walk into lists with numeric indexes. Closes #227 --- dict.go | 26 ++++++++++++++++++++++++++ dict_test.go | 15 +++++++++++++++ functions.go | 1 + 3 files changed, 42 insertions(+) diff --git a/dict.go b/dict.go index 11d943fc..bf82d7de 100644 --- a/dict.go +++ b/dict.go @@ -146,3 +146,29 @@ func deepCopy(i interface{}) interface{} { func mustDeepCopy(i interface{}) (interface{}, error) { return copystructure.Copy(i) } + +func dig(ps ...interface{}) (interface{}, error) { + if len(ps) < 3 { + panic("dig needs at least tree arguments") + } + dict := ps[len(ps)-1].(map[string]interface{}) + def := ps[len(ps)-2] + ks := make([]string, len(ps)-2) + for i := 0; i < len(ks); i++ { + ks[i] = ps[i].(string) + } + + return digFromDict(dict, def, ks) +} + +func digFromDict(dict map[string]interface{}, d interface{}, ks []string) (interface{}, error) { + k, ns := ks[0], ks[1:len(ks)] + step, has := dict[k] + if !has { + return d, nil + } + if len(ns) == 0 { + return step, nil + } + return digFromDict(step.(map[string]interface{}), d, ns) +} diff --git a/dict_test.go b/dict_test.go index f91ee0ea..c829daa5 100644 --- a/dict_test.go +++ b/dict_test.go @@ -293,3 +293,18 @@ func TestMustDeepCopy(t *testing.T) { } } } + +func TestDig(t *testing.T) { + tests := map[string]string{ + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "c" "" $d }}`: "1", + `{{- $d := dict "a" (dict "b" (dict "c" 1)) }}{{ dig "a" "b" "z" "2" $d }}`: "2", + `{{ dict "a" 1 | dig "a" "" }}`: "1", + `{{ dict "a" 1 | dig "z" "2" }}`: "2", + } + + for tpl, expect := range tests { + if err := runt(tpl, expect); err != nil { + t.Error(err) + } + } +} diff --git a/functions.go b/functions.go index c16e9c3e..1a4523bf 100644 --- a/functions.go +++ b/functions.go @@ -297,6 +297,7 @@ var genericMap = map[string]interface{}{ "slice": slice, "mustSlice": mustSlice, "concat": concat, + "dig": dig, // Crypto: "htpasswd": htpasswd, From 4bb6f1cc0f550e6ad5198043c72d5da830673a83 Mon Sep 17 00:00:00 2001 From: Judson Date: Tue, 30 Jun 2020 10:26:08 -0700 Subject: [PATCH 2/3] Documenting `dig` --- docs/dicts.md | 34 ++++++++++++++++++++++++++++++++++ docs/index.md | 3 +-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/dicts.md b/docs/dicts.md index 040cf965..d16281f6 100644 --- a/docs/dicts.md +++ b/docs/dicts.md @@ -88,6 +88,40 @@ inserted. A common idiom in Sprig templates is to uses `pluck... | first` to get the first matching key out of a collection of dictionaries. +## dig + +The `dig` function traverses a nested set of dicts, selecting keys from a list +of values. It returns a default value if any of the keys are not found at the +associated dict. + +``` +dig "user" "role" "humanName" "guest" $dict +``` + +Given a dict structured like +``` +{ + user: { + role: { + humanName: "curator" + } + } +} +``` + +the above would return `"curator"`. If the dict lacked even a `user` field, +the result would be `"guest"`. + +Dig can be very useful in cases where you'd like to avoid guard clauses, +especially since Go's template package's `and` doesn't shortcut. For instance +`and a.maybeNil a.maybeNil.iNeedThis` will always evaluate +`a.maybeNil.iNeedThis`, and panic if `a` lacks a `maybeNil` field.) + +`dig` accepts its dict argument last in order to support pipelining. For instance: +``` +merge a b c | dig "one" "two" "three" "" +``` + ## merge, mustMerge Merge two or more dictionaries into one, giving precedence to the dest dictionary: diff --git a/docs/index.md b/docs/index.md index 87e0c160..b4967836 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ The Sprig library provides over 70 template functions for Go's template language - [Defaults Functions](defaults.md): `default`, `empty`, `coalesce`, `toJson`, `toPrettyJson`, `toRawJson`, `ternary` - [Encoding Functions](encoding.md): `b64enc`, `b64dec`, etc. - [Lists and List Functions](lists.md): `list`, `first`, `uniq`, etc. -- [Dictionaries and Dict Functions](dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `deepCopy`, etc. +- [Dictionaries and Dict Functions](dicts.md): `get`, `set`, `dict`, `hasKey`, `pluck`, `dig`, `deepCopy`, etc. - [Type Conversion Functions](conversion.md): `atoi`, `int64`, `toString`, etc. - [File Path Functions](paths.md): `base`, `dir`, `ext`, `clean`, `isAbs` - [Flow Control Functions](flow_control.md): `fail` @@ -20,4 +20,3 @@ The Sprig library provides over 70 template functions for Go's template language - [Version Comparison Functions](semver.md): `semver`, `semverCompare` - [Reflection](reflection.md): `typeOf`, `kindIs`, `typeIsLike`, etc. - [Cryptographic and Security Functions](crypto.md): `derivePassword`, `sha256sum`, `genPrivateKey`, etc. - From eaeb5ac769579fa931de26f89dfb70c9daf228cc Mon Sep 17 00:00:00 2001 From: Matt Farina Date: Wed, 2 Dec 2020 10:18:34 -0500 Subject: [PATCH 3/3] Fixing spelling in panic --- dict.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dict.go b/dict.go index bf82d7de..ade88969 100644 --- a/dict.go +++ b/dict.go @@ -149,7 +149,7 @@ func mustDeepCopy(i interface{}) (interface{}, error) { func dig(ps ...interface{}) (interface{}, error) { if len(ps) < 3 { - panic("dig needs at least tree arguments") + panic("dig needs at least three arguments") } dict := ps[len(ps)-1].(map[string]interface{}) def := ps[len(ps)-2]