From 5dd9dc8427e38b6c992cac5732c9944195b46874 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Thu, 16 Jul 2020 16:29:27 -0400 Subject: [PATCH] Add test.Kind/kind and test.IsKind/isKind functions Signed-off-by: Dave Henderson --- docs-src/content/functions/test.yml | 71 +++++++++++++++++++ docs/content/functions/test.md | 101 ++++++++++++++++++++++++++++ funcs/test.go | 23 +++++++ funcs/test_test.go | 64 ++++++++++++++++++ 4 files changed, 259 insertions(+) diff --git a/docs-src/content/functions/test.yml b/docs-src/content/functions/test.yml index a2763d12..a29ab03d 100644 --- a/docs-src/content/functions/test.yml +++ b/docs-src/content/functions/test.yml @@ -37,6 +37,77 @@ funcs: template: :1:3: executing "" at : error calling fail: template generation failed $ gomplate -i '{{ test.Fail "something is wrong!" }}' template: :1:7: executing "" at : error calling Fail: template generation failed: something is wrong! + - name: test.IsKind + alias: isKind + description: | + Report whether the argument is of the given Kind. Can be used to render + different templates depending on the kind of data. + + See [the Go `reflect` source code](https://github.com/golang/go/blob/36fcde1676a0d3863cb5f295eed6938cd782fcbb/src/reflect/type.go#L595..L622) + for the complete list, but these are some common values: + + - `string` + - `bool` + - `int`, `int64`, `uint64` + - `float64` + - `slice` + - `map` + - `invalid` (a catch-all, usually just `nil` values) + + In addition, the special kind `number` is accepted by this function, to + represent _any_ numeric kind (whether `float32`, `uint8`, or whatever). + This is useful when the specific numeric type is unknown. + + See also [`test.Kind`](test-kind). + pipeline: true + arguments: + - name: kind + required: true + description: the kind to compare with (see desription for possible values) + - name: value + required: true + description: the value to check + examples: + - | + $ gomplate -i '{{ $data := "hello world" }} + {{- if isKind "string" $data }}{{ $data }} is a string{{ end }}' + hello world is a string + - | + $ gomplate -i '{{ $object := dict "key1" true "key2" "foobar" }} + {{- if test.IsKind "map" $object }} + Got a map: + {{ range $key, $value := $object -}} + - "{{ $key }}": {{ $value }} + {{ end }} + {{ else if test.IsKind "number" $object }} + Got a number: {{ $object }} + {{ end }}' + + Got a map: + - "key1": true + - "key2": foobar + - name: test.Kind + alias: kind + description: | + Report the _kind_ of the given argument. This differs from the _type_ of + the argument in specificity; for example, while a slice of strings may + have a type of `[]string`, the _kind_ of that slice will simply be `slice`. + + If you need to know the precise type of a value, use `printf "%T" $value`. + + See also [`test.IsKind`](test-iskind). + pipeline: true + arguments: + - name: value + required: true + description: the value to check + examples: + - | + $ gomplate -i '{{ kind "hello world" }}' + string + - | + $ gomplate -i '{{ dict "key1" true "key2" "foobar" | test.Kind }}' + map - name: test.Required alias: required description: | diff --git a/docs/content/functions/test.md b/docs/content/functions/test.md index c19ba4c5..0b427c52 100644 --- a/docs/content/functions/test.md +++ b/docs/content/functions/test.md @@ -70,6 +70,107 @@ $ gomplate -i '{{ test.Fail "something is wrong!" }}' template: :1:7: executing "" at : error calling Fail: template generation failed: something is wrong! ``` +## `test.IsKind` + +**Alias:** `isKind` + +Report whether the argument is of the given Kind. Can be used to render +different templates depending on the kind of data. + +See [the Go `reflect` source code](https://github.com/golang/go/blob/36fcde1676a0d3863cb5f295eed6938cd782fcbb/src/reflect/type.go#L595..L622) +for the complete list, but these are some common values: + +- `string` +- `bool` +- `int`, `int64`, `uint64` +- `float64` +- `slice` +- `map` +- `invalid` (a catch-all, usually just `nil` values) + +In addition, the special kind `number` is accepted by this function, to +represent _any_ numeric kind (whether `float32`, `uint8`, or whatever). +This is useful when the specific numeric type is unknown. + +See also [`test.Kind`](test-kind). + +### Usage + +```go +test.IsKind kind value +``` +```go +value | test.IsKind kind +``` + +### Arguments + +| name | description | +|------|-------------| +| `kind` | _(required)_ the kind to compare with (see desription for possible values) | +| `value` | _(required)_ the value to check | + +### Examples + +```console +$ gomplate -i '{{ $data := "hello world" }} +{{- if isKind "string" $data }}{{ $data }} is a string{{ end }}' +hello world is a string +``` +```console +$ gomplate -i '{{ $object := dict "key1" true "key2" "foobar" }} +{{- if test.IsKind "map" $object }} +Got a map: +{{ range $key, $value := $object -}} + - "{{ $key }}": {{ $value }} +{{ end }} +{{ else if test.IsKind "number" $object }} +Got a number: {{ $object }} +{{ end }}' + +Got a map: +- "key1": true +- "key2": foobar +``` + +## `test.Kind` + +**Alias:** `kind` + +Report the _kind_ of the given argument. This differs from the _type_ of +the argument in specificity; for example, while a slice of strings may +have a type of `[]string`, the _kind_ of that slice will simply be `slice`. + +If you need to know the precise type of a value, use `printf "%T" $value`. + +See also [`test.IsKind`](test-iskind). + +### Usage + +```go +test.Kind value +``` +```go +value | test.Kind +``` + +### Arguments + +| name | description | +|------|-------------| +| `value` | _(required)_ the value to check | + +### Examples + +```console +$ gomplate -i '{{ kind "hello world" }}' +string +``` +```console +$ gomplate -i '{{ dict "key1" true "key2" "foobar" | test.Kind }}' +map +``` + ## `test.Required` **Alias:** `required` diff --git a/funcs/test.go b/funcs/test.go index 265ede10..820d4f17 100644 --- a/funcs/test.go +++ b/funcs/test.go @@ -1,6 +1,7 @@ package funcs import ( + "reflect" "sync" "github.com/hairyhenderson/gomplate/v3/conv" @@ -28,6 +29,8 @@ func AddTestFuncs(f map[string]interface{}) { f["fail"] = TestNS().Fail f["required"] = TestNS().Required f["ternary"] = TestNS().Ternary + f["kind"] = TestNS().Kind + f["isKind"] = TestNS().IsKind } // TestFuncs - @@ -85,3 +88,23 @@ func (f *TestFuncs) Ternary(tval, fval, b interface{}) interface{} { } return fval } + +// Kind - return the kind of the argument +func (f *TestFuncs) Kind(arg interface{}) string { + return reflect.ValueOf(arg).Kind().String() +} + +// IsKind - return whether or not the argument is of the given kind +func (f *TestFuncs) IsKind(kind string, arg interface{}) bool { + k := f.Kind(arg) + if kind == "number" { + switch k { + case "int", "int8", "int16", "int32", "int64", + "uint", "uint8", "uint16", "uint32", "uint64", "uintptr", + "float32", "float64", + "complex64", "complex128": + kind = k + } + } + return k == kind +} diff --git a/funcs/test_test.go b/funcs/test_test.go index 1a6868d3..5fa672a5 100644 --- a/funcs/test_test.go +++ b/funcs/test_test.go @@ -78,3 +78,67 @@ func TestTernary(t *testing.T) { assert.Equal(t, d.expected, f.Ternary(d.tval, d.fval, d.b)) } } + +func TestKind(t *testing.T) { + f := TestNS() + testdata := []struct { + arg interface{} + expected string + }{ + {"foo", "string"}, + {nil, "invalid"}, + {false, "bool"}, + {[]string{"foo", "bar"}, "slice"}, + {map[string]string{"foo": "bar"}, "map"}, + {42, "int"}, + {42.0, "float64"}, + {uint(42), "uint"}, + {struct{}{}, "struct"}, + } + for _, d := range testdata { + assert.Equal(t, d.expected, f.Kind(d.arg)) + } +} + +func TestIsKind(t *testing.T) { + f := TestNS() + truedata := []struct { + arg interface{} + kind string + }{ + {"foo", "string"}, + {nil, "invalid"}, + {false, "bool"}, + {[]string{"foo", "bar"}, "slice"}, + {map[string]string{"foo": "bar"}, "map"}, + {42, "int"}, + {42.0, "float64"}, + {uint(42), "uint"}, + {struct{}{}, "struct"}, + {42.0, "number"}, + {42, "number"}, + {uint32(64000), "number"}, + {complex128(64000), "number"}, + } + for _, d := range truedata { + assert.True(t, f.IsKind(d.kind, d.arg)) + } + + falsedata := []struct { + arg interface{} + kind string + }{ + {"foo", "bool"}, + {nil, "struct"}, + {false, "string"}, + {[]string{"foo", "bar"}, "map"}, + {map[string]string{"foo": "bar"}, "int"}, + {42, "int64"}, + {42.0, "float32"}, + {uint(42), "int"}, + {struct{}{}, "interface"}, + } + for _, d := range falsedata { + assert.False(t, f.IsKind(d.kind, d.arg)) + } +}