From 2c2bd3429bec46c051ebcce09e2e5adde2237838 Mon Sep 17 00:00:00 2001 From: Hugo <10965479+hugoghx@users.noreply.github.com> Date: Fri, 22 Mar 2024 15:17:40 +0000 Subject: [PATCH] feat(values): Add methods to get bool and time values from proto --- internal/values/proto.go | 47 ++++++++++++ internal/values/proto_test.go | 130 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+) diff --git a/internal/values/proto.go b/internal/values/proto.go index 7d553b7..d6513d9 100644 --- a/internal/values/proto.go +++ b/internal/values/proto.go @@ -5,6 +5,7 @@ package values import ( "fmt" + "time" "google.golang.org/protobuf/types/known/structpb" ) @@ -37,6 +38,52 @@ func GetStringValue(in *structpb.Struct, k string, required bool) (string, error return s, nil } +// GetBoolValue returns a boolean value and no error if the given key is found +// in the provided proto struct input. An error is returned if the key is not +// found and required is true, or the value type is not a boolean. +func GetBoolValue(in *structpb.Struct, k string, required bool) (bool, error) { + mv := in.GetFields() + v, ok := mv[k] + if !ok { + if required { + return false, fmt.Errorf("missing required value %q", k) + } + + return false, nil + } + + b, ok := v.AsInterface().(bool) + if !ok { + return false, fmt.Errorf("unexpected type for value %q: want bool, got %T", k, v.AsInterface()) + } + + return b, nil +} + +// GetTimeValue returns a time.Time object and no error if the given key is +// found in the provided proto struct input. An error is returned if the value +// in the struct isn't a string or the value is not parsable by time.Parse. The +// expected time layout is RFC3339Nano. +func GetTimeValue(in *structpb.Struct, k string) (time.Time, error) { + mv := in.GetFields() + v, ok := mv[k] + if !ok { + return time.Time{}, nil + } + + tRaw, ok := v.AsInterface().(string) + if !ok { + return time.Time{}, fmt.Errorf("unexpected type for value %q: want string, got %T", k, v.AsInterface()) + } + + t, err := time.Parse(time.RFC3339Nano, tRaw) + if err != nil { + return time.Time{}, fmt.Errorf("could not parse time in value %q: %w", k, err) + } + + return t, nil +} + // StructFields returns a map[string]struct{} of the proto struct input. func StructFields(s *structpb.Struct) map[string]struct{} { m := make(map[string]struct{}, len(s.GetFields())) diff --git a/internal/values/proto_test.go b/internal/values/proto_test.go index 0aa4b53..b38fdd9 100644 --- a/internal/values/proto_test.go +++ b/internal/values/proto_test.go @@ -5,6 +5,7 @@ package values import ( "testing" + "time" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" @@ -78,3 +79,132 @@ func TestGetStringValue(t *testing.T) { }) } } + +func TestGetBoolValue(t *testing.T) { + cases := []struct { + name string + in map[string]any + key string + required bool + expected bool + expectedErr string + }{ + { + name: "required missing", + in: map[string]any{}, + key: "foo", + required: true, + expectedErr: "missing required value \"foo\"", + }, + { + name: "optional missing", + in: map[string]any{}, + key: "foo", + expected: false, + }, + { + name: "non-bool value", + in: map[string]any{ + "foo": "bar", + }, + key: "foo", + expectedErr: "unexpected type for value \"foo\": want bool, got string", + }, + { + name: "good", + in: map[string]any{ + "foo": true, + }, + key: "foo", + expected: true, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + input, err := structpb.NewStruct(tc.in) + require.NoError(err) + + actual, err := GetBoolValue(input, tc.key, tc.required) + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expected, actual) + }) + } +} + +func TestGetTimeValue(t *testing.T) { + staticTime := time.Now() + + cases := []struct { + name string + in map[string]any + key string + expected time.Time + expectedErr string + }{ + { + name: "missing", + in: map[string]any{}, + key: "foo", + expected: time.Time{}, + }, + { + name: "non-time value", + in: map[string]any{ + "foo": 1, + }, + key: "foo", + expectedErr: "unexpected type for value \"foo\": want string, got float64", + }, + { + name: "bad parse", + in: map[string]any{ + "foo": "bar", + }, + key: "foo", + expectedErr: "could not parse time in value \"foo\": parsing time \"bar\" as \"2006-01-02T15:04:05.999999999Z07:00\": cannot parse \"bar\" as \"2006\"", + }, + { + name: "good", + in: map[string]any{ + "foo": staticTime.Format(time.RFC3339Nano), + }, + key: "foo", + expected: func() time.Time { + u, err := time.Parse(time.RFC3339Nano, staticTime.Format(time.RFC3339Nano)) + if err != nil { + panic(err) + } + + return u + }(), + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + input, err := structpb.NewStruct(tc.in) + require.NoError(err) + + actual, err := GetTimeValue(input, tc.key) + if tc.expectedErr != "" { + require.EqualError(err, tc.expectedErr) + return + } + + require.NoError(err) + require.Equal(tc.expected, actual) + }) + } +}