From 6d3f3999059c00e7c9d9308529ebb21ca99f66e4 Mon Sep 17 00:00:00 2001 From: "Antonio M. Amaya Calvo" Date: Tue, 1 Aug 2023 16:43:47 +0200 Subject: [PATCH] Add timeutil package (#186) Co-authored-by: nicolaasuni-vonage --- VERSION | 2 +- examples/service/go.mod | 4 +- examples/service/go.sum | 4 +- go.mod | 2 +- go.sum | 4 +- pkg/timeutil/duration.go | 49 ++++++ pkg/timeutil/duration_test.go | 241 ++++++++++++++++++++++++++ pkg/timeutil/example_duration_test.go | 50 ++++++ pkg/timeutil/timeutil.go | 2 + 9 files changed, 350 insertions(+), 8 deletions(-) create mode 100644 pkg/timeutil/duration.go create mode 100644 pkg/timeutil/duration_test.go create mode 100644 pkg/timeutil/example_duration_test.go create mode 100644 pkg/timeutil/timeutil.go diff --git a/VERSION b/VERSION index 7402710c..32a6ce3c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.75.9 +1.76.0 diff --git a/examples/service/go.mod b/examples/service/go.mod index 57f14d4a..0513527e 100644 --- a/examples/service/go.mod +++ b/examples/service/go.mod @@ -5,7 +5,7 @@ go 1.20 replace github.com/Vonage/gosrvlib => ../.. require ( - github.com/Vonage/gosrvlib v1.75.9 + github.com/Vonage/gosrvlib v1.76.0 github.com/golang/mock v1.6.0 github.com/jstemmer/go-junit-report v1.0.0 github.com/prometheus/client_golang v1.16.0 @@ -86,7 +86,7 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.11.0 // indirect - golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect + golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect diff --git a/examples/service/go.sum b/examples/service/go.sum index 4f6a05eb..a1552037 100644 --- a/examples/service/go.sum +++ b/examples/service/go.sum @@ -847,8 +847,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= -golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/go.mod b/go.mod index 343c6cd8..558cff72 100644 --- a/go.mod +++ b/go.mod @@ -112,7 +112,7 @@ require ( go.etcd.io/etcd/client/v3 v3.5.9 // indirect go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.11.0 // indirect - golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 // indirect + golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b // indirect golang.org/x/mod v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect diff --git a/go.sum b/go.sum index 4cb00e03..012bf2f6 100644 --- a/go.sum +++ b/go.sum @@ -916,8 +916,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691 h1:/yRP+0AN7mf5DkD3BAI6TOFnd51gEoDEb8o35jIFtgw= -golang.org/x/exp v0.0.0-20230728194245-b0cb94b80691/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b h1:r+vk0EmXNmekl0S0BascoeeoHk/L7wmaW2QF90K+kYI= +golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/pkg/timeutil/duration.go b/pkg/timeutil/duration.go new file mode 100644 index 00000000..d0b06948 --- /dev/null +++ b/pkg/timeutil/duration.go @@ -0,0 +1,49 @@ +package timeutil + +import ( + "encoding/json" + "fmt" + "time" +) + +// Duration is an alias for the standard time.Duration. +type Duration time.Duration + +// String returns a string representing the duration in the form "72h3m0.5s". +// It is a wrapper for time.Duration.String(). +func (d Duration) String() string { + return time.Duration(d).String() +} + +// MarshalJSON returns d as the JSON encoding of d. +// It encodes the time.Duration in human readable format (e.g.: 20s, 1h, ...). +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) //nolint:wrapcheck +} + +// UnmarshalJSON sets *d to a copy of data. +// It converts human readable time duration format (e.g.: 20s, 1h, ...) in standard time.Duration. +func (d *Duration) UnmarshalJSON(data []byte) error { + var v any + + if err := json.Unmarshal(data, &v); err != nil { + return err //nolint:wrapcheck + } + + switch value := v.(type) { + case float64: + *d = Duration(value) + return nil + case string: + aux, err := time.ParseDuration(value) + if err != nil { + return fmt.Errorf("unable to parse the time duration %s :%w", value, err) + } + + *d = Duration(aux) + + return nil + default: + return fmt.Errorf("invalid time duration type: %v", value) + } +} diff --git a/pkg/timeutil/duration_test.go b/pkg/timeutil/duration_test.go new file mode 100644 index 00000000..c966c7d2 --- /dev/null +++ b/pkg/timeutil/duration_test.go @@ -0,0 +1,241 @@ +package timeutil + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDuration_MarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + dur Duration + want []byte + }{ + { + name: "seconds", + dur: Duration(13 * time.Second), + want: []byte(`"13s"`), + }, + { + name: "minutes", + dur: Duration(17 * time.Minute), + want: []byte(`"17m0s"`), + }, + { + name: "hours", + dur: Duration(7*time.Hour + 11*time.Minute + 13*time.Second), + want: []byte(`"7h11m13s"`), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := tt.dur.MarshalJSON() + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_json_Marshal(t *testing.T) { + t.Parallel() + + type testData struct { + Time Duration + } + + tests := []struct { + name string + data testData + want []byte + }{ + { + name: "seconds", + data: testData{Time: Duration(13 * time.Second)}, + want: []byte(`{"Time":"13s"}`), + }, + { + name: "minutes", + data: testData{Time: Duration(17 * time.Minute)}, + want: []byte(`{"Time":"17m0s"}`), + }, + { + name: "hours", + data: testData{Time: Duration(7*time.Hour + 11*time.Minute + 13*time.Second)}, + want: []byte(`{"Time":"7h11m13s"}`), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got, err := json.Marshal(tt.data) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestDuration_UnmarshalJSON(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + data []byte + want Duration + wantErr bool + }{ + { + name: "seconds", + data: []byte(`"13s"`), + want: Duration(13 * time.Second), + }, + { + name: "minutes", + data: []byte(`"17m0s"`), + want: Duration(17 * time.Minute), + }, + { + name: "hours", + data: []byte(`"73h0m0s"`), + want: Duration(73 * time.Hour), + }, + { + name: "number", + data: []byte(`123456789`), + want: Duration(123456789), + }, + { + name: "zero number", + data: []byte(`0`), + want: Duration(0), + }, + { + name: "negative number", + data: []byte(`-17`), + want: Duration(-17), + }, + { + name: "empty", + data: []byte(``), + wantErr: true, + }, + { + name: "empty string", + data: []byte(`""`), + wantErr: true, + }, + { + name: "invalid string", + data: []byte(`"-"`), + wantErr: true, + }, + { + name: "invalid type", + data: []byte(`{"a":"b"}`), + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var dur Duration + + err := dur.UnmarshalJSON(tt.data) + require.Equal(t, tt.wantErr, err != nil, "error = %v, wantErr %v", err, tt.wantErr) + require.Equal(t, int64(tt.want), int64(dur)) + }) + } +} + +func Test_json_Unmarshal(t *testing.T) { + t.Parallel() + + type testData struct { + Time Duration + } + + tests := []struct { + name string + data []byte + want Duration + wantErr bool + }{ + { + name: "seconds", + data: []byte(`{"Time":"13s"}`), + want: Duration(13 * time.Second), + }, + { + name: "minutes", + data: []byte(`{"Time":"17m0s"}`), + want: Duration(17 * time.Minute), + }, + { + name: "hours", + data: []byte(`{"Time":"7h11m13s"}`), + want: Duration(7*time.Hour + 11*time.Minute + 13*time.Second), + }, + { + name: "number", + data: []byte(`{"Time":123456789}`), + want: Duration(123456789), + }, + { + name: "zero number", + data: []byte(`{"Time":0}`), + want: Duration(0), + }, + { + name: "negative number", + data: []byte(`{"Time":-17}`), + want: Duration(-17), + }, + { + name: "null", + data: []byte(`{"Time":null}`), + wantErr: true, + }, + { + name: "empty string", + data: []byte(`{"Time":""}`), + wantErr: true, + }, + { + name: "invalid string", + data: []byte(`{"Time":"-"}`), + wantErr: true, + }, + { + name: "invalid type", + data: []byte(`{"Time":{"a":"b"}}`), + wantErr: true, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var d testData + + err := json.Unmarshal(tt.data, &d) + require.Equal(t, tt.wantErr, err != nil, "error = %v, wantErr %v", err, tt.wantErr) + require.Equal(t, int64(tt.want), int64(d.Time)) + }) + } +} diff --git a/pkg/timeutil/example_duration_test.go b/pkg/timeutil/example_duration_test.go new file mode 100644 index 00000000..2c959ea3 --- /dev/null +++ b/pkg/timeutil/example_duration_test.go @@ -0,0 +1,50 @@ +package timeutil_test + +import ( + "encoding/json" + "fmt" + "log" + "time" + + "github.com/Vonage/gosrvlib/pkg/timeutil" +) + +func ExampleDuration_MarshalJSON() { + type testData struct { + Time timeutil.Duration + } + + data := testData{ + Time: timeutil.Duration(7*time.Hour + 11*time.Minute + 13*time.Second), + } + + enc, err := json.Marshal(data) + if err != nil { + log.Fatal(err) + } + + fmt.Println(string(enc)) + + // Output: + // {"Time":"7h11m13s"} +} + +func ExampleDuration_UnmarshalJSON() { + type testData struct { + Time timeutil.Duration + } + + enc := []byte(`{"Time":"7h11m13s"}`) + + var data testData + + err := json.Unmarshal(enc, &data) + if err != nil { + log.Fatal(err) + } + + fmt.Println(data.Time.String()) + + // Output: + // 7h11m13s +} diff --git a/pkg/timeutil/timeutil.go b/pkg/timeutil/timeutil.go new file mode 100644 index 00000000..91347f49 --- /dev/null +++ b/pkg/timeutil/timeutil.go @@ -0,0 +1,2 @@ +// Package timeutil adds utility functions to the standard time library. +package timeutil