From a187c92f0ece7c0b7ac10795713b0cc41402ef0a Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Tue, 22 May 2018 16:11:31 -0700 Subject: [PATCH] implement datetime functions --- lang/funcs/datetime.go | 73 +++++++++++++++++++++++++++++++ lang/funcs/datetime_test.go | 87 +++++++++++++++++++++++++++++++++++++ lang/functions.go | 4 +- 3 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 lang/funcs/datetime.go create mode 100644 lang/funcs/datetime_test.go diff --git a/lang/funcs/datetime.go b/lang/funcs/datetime.go new file mode 100644 index 000000000..8e15fa00b --- /dev/null +++ b/lang/funcs/datetime.go @@ -0,0 +1,73 @@ +package funcs + +import ( + "time" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// TimestampFunc constructs a function that returns a string representation of the current date and time. +var TimestampFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil + }, +}) + +// TimeStampFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. +var TimeAddFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "timestamp", + Type: cty.String, + }, + { + Name: "duration", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + ts, err := time.Parse(time.RFC3339, args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), err + } + duration, err := time.ParseDuration(args[1].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil + }, +}) + +// Timestamp returns a string representation of the current date and time. +// +// In the Terraform language, timestamps are conventionally represented as +// strings using [RFC 3339](https://tools.ietf.org/html/rfc3339) +// "Date and Time format" syntax, and so `timestamp` returns a string +// in this format. +func Timestamp() (cty.Value, error) { + return TimestampFunc.Call([]cty.Value{}) +} + +// Timeadd adds a duration to a timestamp, returning a new timestamp. +// +// In the Terraform language, timestamps are conventionally represented as +// strings using [RFC 3339](https://tools.ietf.org/html/rfc3339) +// "Date and Time format" syntax. `timeadd` requires the `timestamp` argument +// to be a string conforming to this syntax. +// +// `duration` is a string representation of a time difference, consisting of +// sequences of number and unit pairs, like `"1.5h"` or `1h30m`. The accepted +// units are `ns`, `us` (or `µs`), `"ms"`, `"s"`, `"m"`, and `"h"`. The first +// number may be negative to indicate a negative duration, like `"-2h5m"`. +// +// The result is a string, also in RFC 3339 format, representing the result +// of adding the given direction to the given timestamp. + +func TimeAdd(timestamp cty.Value, duration cty.Value) (cty.Value, error) { + return TimeAddFunc.Call([]cty.Value{timestamp, duration}) +} diff --git a/lang/funcs/datetime_test.go b/lang/funcs/datetime_test.go new file mode 100644 index 000000000..bc40eeddc --- /dev/null +++ b/lang/funcs/datetime_test.go @@ -0,0 +1,87 @@ +package funcs + +import ( + "fmt" + "testing" + "time" + + "github.com/zclconf/go-cty/cty" +) + +func TestTimestamp(t *testing.T) { + currentTime := time.Now().UTC() + result, err := Timestamp() + if err != nil { + t.Fatalf("err: %s", err) + } + resultTime, err := time.Parse(time.RFC3339, result.AsString()) + if err != nil { + t.Fatalf("Error parsing timestamp: %s", err) + } + + if resultTime.Sub(currentTime).Seconds() > 10.0 { + t.Fatalf("Timestamp Diff too large. Expected: %s\nReceived: %s", currentTime.Format(time.RFC3339), result.AsString()) + } + +} + +func TestTimeadd(t *testing.T) { + tests := []struct { + Time cty.Value + Duration cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("2017-11-22T00:00:00Z"), + cty.StringVal("1s"), + cty.StringVal("2017-11-22T00:00:01Z"), + false, + }, + { + cty.StringVal("2017-11-22T00:00:00Z"), + cty.StringVal("10m1s"), + cty.StringVal("2017-11-22T00:10:01Z"), + false, + }, + { // also support subtraction + cty.StringVal("2017-11-22T00:00:00Z"), + cty.StringVal("-1h"), + cty.StringVal("2017-11-21T23:00:00Z"), + false, + }, + { // Invalid format timestamp + cty.StringVal("2017-11-22"), + cty.StringVal("-1h"), + cty.UnknownVal(cty.String), + true, + }, + { // Invalid format duration (day is not supported by ParseDuration) + cty.StringVal("2017-11-22T00:00:00Z"), + cty.StringVal("1d"), + cty.UnknownVal(cty.String), + true, + }, + } + + for _, test := range tests { + t.Run(fmt.Sprintf("TimeAdd(%#v, %#v)", test.Time, test.Duration), func(t *testing.T) { + got, err := TimeAdd(test.Time, test.Duration) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} diff --git a/lang/functions.go b/lang/functions.go index 695c55eba..3ec24e741 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -85,8 +85,8 @@ func (s *Scope) Functions() map[string]function.Function { "sort": funcs.SortFunc, "split": funcs.SplitFunc, "substr": stdlib.SubstrFunc, - "timestamp": unimplFunc, // TODO - "timeadd": unimplFunc, // TODO + "timestamp": funcs.TimestampFunc, + "timeadd": funs.TimeaddFunc, "title": unimplFunc, // TODO "transpose": unimplFunc, // TODO "trimspace": unimplFunc, // TODO