Skip to content

Commit 134b326

Browse files
authored
feat(core): add support for relative date parsing (#1366)
1 parent fe43518 commit 134b326

12 files changed

+220
-14
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲
2+
🟥🟥🟥 STDERR️️ 🟥🟥🟥️
3+
Date parsing
4+
5+
You have two ways for managing date in the CLI: Absolute and Relative
6+
7+
- Absolute time
8+
9+
Absolute time refers to a specific and absolute point in time.
10+
CLI uses RFC3339 to parse those time and pass a time.Time go structure to the underlying functions.
11+
12+
Example: "2006-01-02T15:04:05Z07:00"
13+
14+
- Relative time
15+
16+
Relative time refers to a time calculated from adding a given duration to the time when a command is launched.
17+
18+
Example:
19+
- +1d4m => current time plus 1 day and 4 minutes
20+
- -1d4m => current time minus 1 day and 4 minutes
21+
22+
- Units of time
23+
24+
Nanosecond: ns
25+
Microsecond: us, µs (U+00B5 = micro symbol), μs (U+03BC = Greek letter mu)
26+
Millisecond: ms
27+
Second: s, sec, second, seconds
28+
Minute: m, min, minute, minutes
29+
Hour: h, hr, hour, hours
30+
Day: d, day, days
31+
Week: w, wk, week, weeks
32+
Month: mo, mon, month, months
33+
Year: y, yr, year, years
34+
35+
USAGE:
36+
scw help date
37+
38+
FLAGS:
39+
-h, --help help for date
40+
41+
GLOBAL FLAGS:
42+
-c, --config string The path to the config file
43+
-D, --debug Enable debug mode
44+
-o, --output string Output format: json or human, see 'scw help output' for more info (default "human")
45+
-p, --profile string The config profile to use

cmd/scw/testdata/test-all-usage-help-usage.golden

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ USAGE:
66
scw help <command>
77

88
AVAILABLE COMMANDS:
9+
date Get help about how date parsing works in the CLI
910
output Get help about how the CLI output works
1011

1112
FLAGS:

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
github.com/getsentry/raven-go v0.2.0
1818
github.com/gorilla/websocket v1.4.2
1919
github.com/hashicorp/go-version v1.2.0
20+
github.com/karrick/tparse v2.4.2+incompatible
2021
github.com/kr/pretty v0.1.0 // indirect
2122
github.com/mattn/go-colorable v0.1.4
2223
github.com/mattn/go-isatty v0.0.11

go.sum

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09
3939
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
4040
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
4141
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
42+
github.com/karrick/tparse v1.0.0 h1:qVJoscl1sG/UodDmNjjY6cSIun7s541PNNE42dqstfg=
43+
github.com/karrick/tparse v2.4.2+incompatible h1:+cW306qKAzrASC5XieHkgN7/vPaGKIuK62Q7nI7DIRc=
44+
github.com/karrick/tparse v2.4.2+incompatible/go.mod h1:ASPA+vrIcN1uEW6BZg8vfWbzm69ODPSYZPU6qJyfdK0=
4245
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
4346
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
4447
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=

internal/args/errors.go

+18
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,21 @@ func missingIndices(index, length int) string {
187187
}
188188
return strings.Join(s, ",")
189189
}
190+
191+
type CannotParseDateError struct {
192+
ArgValue string
193+
AbsoluteTimeParseError error
194+
RelativeTimeParseError error
195+
}
196+
197+
func (e *CannotParseDateError) Error() string {
198+
return fmt.Sprintf(`date parsing error: could not parse %s`, e.ArgValue)
199+
}
200+
201+
type CannotParseBoolError struct {
202+
Value string
203+
}
204+
205+
func (e *CannotParseBoolError) Error() string {
206+
return "invalid boolean value"
207+
}

internal/args/unmarshal.go

+28-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"time"
1515

1616
"github.com/dustin/go-humanize"
17+
"github.com/karrick/tparse"
1718
"github.com/scaleway/scaleway-sdk-go/scw"
1819
"github.com/scaleway/scaleway-sdk-go/strcase"
1920
"github.com/scaleway/scaleway-sdk-go/validation"
@@ -25,6 +26,8 @@ type Unmarshaler interface {
2526

2627
type UnmarshalFunc func(value string, dest interface{}) error
2728

29+
var TestForceNow *time.Time
30+
2831
var unmarshalFuncs = map[reflect.Type]UnmarshalFunc{
2932
reflect.TypeOf((*scw.Size)(nil)).Elem(): func(value string, dest interface{}) error {
3033
// Only support G, GB for now (case insensitive).
@@ -61,13 +64,32 @@ var unmarshalFuncs = map[reflect.Type]UnmarshalFunc{
6164

6265
reflect.TypeOf((*time.Time)(nil)).Elem(): func(value string, dest interface{}) error {
6366
// Handle absolute time
64-
t, err := time.Parse(time.RFC3339, value)
65-
if err != nil {
66-
return err
67+
absoluteTimeParsed, absoluteErr := time.Parse(time.RFC3339, value)
68+
if absoluteErr == nil {
69+
*(dest.(*time.Time)) = absoluteTimeParsed
70+
return nil
6771
}
6872

69-
*(dest.(*time.Time)) = t
70-
return nil
73+
// Handle relative time
74+
if value[0] != '+' && value[0] != '-' {
75+
value = "+" + value
76+
}
77+
m := map[string]time.Time{
78+
"t": time.Now(),
79+
}
80+
if TestForceNow != nil {
81+
m["t"] = *TestForceNow
82+
}
83+
relativeTimeParsed, relativeErr := tparse.ParseWithMap(time.RFC3339, "t"+value, m)
84+
if relativeErr == nil {
85+
*(dest.(*time.Time)) = relativeTimeParsed
86+
return nil
87+
}
88+
return &CannotParseDateError{
89+
ArgValue: value,
90+
AbsoluteTimeParseError: absoluteErr,
91+
RelativeTimeParseError: relativeErr,
92+
}
7193
},
7294
}
7395

@@ -370,7 +392,7 @@ func unmarshalScalar(value string, dest reflect.Value) error {
370392
case "false":
371393
dest.SetBool(false)
372394
default:
373-
return fmt.Errorf("invalid boolean value")
395+
return &CannotParseBoolError{Value: value}
374396
}
375397
return nil
376398
case reflect.String:

internal/args/unmarshal_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import (
1010
"github.com/stretchr/testify/assert"
1111
)
1212

13+
func init() {
14+
TestForceNow = scw.TimePtr(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
15+
}
16+
1317
func TestUnmarshalStruct(t *testing.T) {
1418
type TestCase struct {
1519
args []string
@@ -247,6 +251,32 @@ func TestUnmarshalStruct(t *testing.T) {
247251
},
248252
}))
249253

254+
t.Run("Relative date positive", run(TestCase{
255+
args: []string{
256+
"time=+1m1s",
257+
},
258+
expected: &WellKnownTypes{
259+
Time: time.Date(1970, 01, 01, 0, 1, 1, 0, time.UTC),
260+
},
261+
}))
262+
263+
t.Run("Relative date negative", run(TestCase{
264+
args: []string{
265+
"time=-1m1s",
266+
},
267+
expected: &WellKnownTypes{
268+
Time: time.Date(1969, 12, 31, 23, 58, 59, 0, time.UTC),
269+
},
270+
}))
271+
272+
t.Run("Unknown relative date markers", run(TestCase{
273+
data: &time.Time{},
274+
args: []string{
275+
"time=-1R",
276+
},
277+
error: `cannot unmarshal arg 'time=-1R': cannot set nested field for unmarshalable type time.Time`,
278+
}))
279+
250280
t.Run("nested-basic", run(TestCase{
251281
args: []string{
252282
"basic.string=test",

internal/core/cobra_utils.go

+21-8
Original file line numberDiff line numberDiff line change
@@ -177,14 +177,27 @@ func handleUnmarshalErrors(cmd *Command, unmarshalErr *args.UnmarshalArgError) e
177177

178178
switch e := wrappedErr.(type) {
179179
case *args.CannotUnmarshalError:
180-
hint := ""
181-
if _, ok := e.Dest.(*bool); ok {
182-
hint = "Possible values: true, false"
183-
}
184-
185-
return &CliError{
186-
Err: fmt.Errorf("invalid value for '%s' argument: %s", unmarshalErr.ArgName, e.Err),
187-
Hint: hint,
180+
switch e.Err.(type) {
181+
case *args.CannotParseBoolError:
182+
return &CliError{
183+
Err: fmt.Errorf(""),
184+
Message: fmt.Sprintf("invalid value for '%s' argument: invalid boolean value", unmarshalErr.ArgName),
185+
Hint: "Possible values: true, false",
186+
}
187+
case *args.CannotParseDateError:
188+
dateErr := e.Err.(*args.CannotParseDateError)
189+
return &CliError{
190+
Err: fmt.Errorf("date parsing error: %s", dateErr.ArgValue),
191+
Message: fmt.Sprintf("could not parse %s as either an absolute time (RFC3339) nor a relative time (+/-)RFC3339", dateErr.ArgValue),
192+
Details: fmt.Sprintf(`Absolute time error: %s
193+
Relative time error: %s
194+
`, dateErr.AbsoluteTimeParseError, dateErr.RelativeTimeParseError),
195+
Hint: "Run `scw help date` to learn more about date parsing",
196+
}
197+
default:
198+
return &CliError{
199+
Err: fmt.Errorf("invalid value for '%s' argument: %s", unmarshalErr.ArgName, e.Err),
200+
}
188201
}
189202

190203
case *args.UnknownArgError, *args.InvalidArgNameError:

internal/core/cobra_utils_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"reflect"
77
"testing"
8+
"time"
89

910
"github.com/scaleway/scaleway-cli/internal/args"
1011
)
@@ -14,6 +15,10 @@ type testType struct {
1415
Tag string
1516
}
1617

18+
type testDate struct {
19+
Date *time.Time
20+
}
21+
1722
func testGetCommands() *Commands {
1823
return NewCommands(
1924
&Command{
@@ -64,6 +69,16 @@ func testGetCommands() *Commands {
6469
return res, nil
6570
},
6671
},
72+
&Command{
73+
Namespace: "test",
74+
Resource: "date",
75+
ArgsType: reflect.TypeOf(testDate{}),
76+
AllowAnonymousClient: true,
77+
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
78+
a := argsI.(*testDate)
79+
return a.Date, nil
80+
},
81+
},
6782
)
6883
}
6984

@@ -91,6 +106,22 @@ func Test_handleUnmarshalErrors(t *testing.T) {
91106
}),
92107
),
93108
}))
109+
110+
t.Run("relative date", Test(&TestConfig{
111+
Commands: testGetCommands(),
112+
Cmd: "scw test date date=+3R",
113+
Check: TestCheckCombine(
114+
TestCheckExitCode(1),
115+
TestCheckError(&CliError{
116+
Message: "could not parse +3R as either an absolute time (RFC3339) nor a relative time (+/-)RFC3339",
117+
Details: `Absolute time error: parsing time "+3R" as "2006-01-02T15:04:05Z07:00": cannot parse "+3R" as "2006"
118+
Relative time error: unknown unit in duration: "R"
119+
`,
120+
Err: fmt.Errorf("date parsing error: +3R"),
121+
Hint: "Run `scw help date` to learn more about date parsing",
122+
}),
123+
),
124+
}))
94125
}
95126

96127
func Test_RawArgs(t *testing.T) {

internal/core/testing.go

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/dnaeon/go-vcr/cassette"
2222
"github.com/dnaeon/go-vcr/recorder"
2323
"github.com/hashicorp/go-version"
24+
args "github.com/scaleway/scaleway-cli/internal/args"
2425
"github.com/scaleway/scaleway-cli/internal/human"
2526
"github.com/scaleway/scaleway-cli/internal/interactive"
2627
"github.com/scaleway/scaleway-sdk-go/api/test/v1"
@@ -284,6 +285,9 @@ func Test(config *TestConfig) func(t *testing.T) {
284285
testLogger.level = logger.LogLevelDebug
285286
}
286287

288+
// We need to set up this variable to ensure that relative date parsing stay consistent
289+
args.TestForceNow = scw.TimePtr(time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC))
290+
287291
// Because human marshal of date is relative (e.g 3 minutes ago) we must make sure it stay consistent for golden to works.
288292
// Here we return a constant string. We may need to find a better place to put this.
289293
human.RegisterMarshalerFunc(time.Time{}, func(i interface{}, opt *human.MarshalOpt) (string, error) {

internal/namespaces/help/custom.go

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func GetCommands() *core.Commands {
1111
return core.NewCommands(
1212
helpRoot(),
1313
newHelpCommand("output", shortOutput, longOutput),
14+
newHelpCommand("date", shortDate, longDate),
1415
)
1516
}
1617

internal/namespaces/help/date.go

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package help
2+
3+
const (
4+
shortDate = "Get help about how date parsing works in the CLI"
5+
longDate = `Date parsing
6+
7+
You have two ways for managing date in the CLI: Absolute and Relative
8+
9+
- Absolute time
10+
11+
Absolute time refers to a specific and absolute point in time.
12+
CLI uses RFC3339 to parse those time and pass a time.Time go structure to the underlying functions.
13+
14+
Example: "2006-01-02T15:04:05Z07:00"
15+
16+
- Relative time
17+
18+
Relative time refers to a time calculated from adding a given duration to the time when a command is launched.
19+
20+
Example:
21+
- +1d4m => current time plus 1 day and 4 minutes
22+
- -1d4m => current time minus 1 day and 4 minutes
23+
24+
- Units of time
25+
26+
Nanosecond: ns
27+
Microsecond: us, µs (U+00B5 = micro symbol), μs (U+03BC = Greek letter mu)
28+
Millisecond: ms
29+
Second: s, sec, second, seconds
30+
Minute: m, min, minute, minutes
31+
Hour: h, hr, hour, hours
32+
Day: d, day, days
33+
Week: w, wk, week, weeks
34+
Month: mo, mon, month, months
35+
Year: y, yr, year, years
36+
`
37+
)

0 commit comments

Comments
 (0)