Skip to content

Commit fc07a8c

Browse files
authored
Merge pull request #1959 from horockey/issue_1958
Feat:(issue_1958) Add support for multiple layouts to TimestampFlag
2 parents 127cf54 + ead7db0 commit fc07a8c

File tree

7 files changed

+325
-27
lines changed

7 files changed

+325
-27
lines changed

args_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestArgumentsSubcommand(t *testing.T) {
6868
Max: 1,
6969
Destination: &tval,
7070
Config: TimestampConfig{
71-
Layout: time.RFC3339,
71+
Layouts: []string{time.RFC3339},
7272
},
7373
},
7474
&StringArg{

command_test.go

+6-3
Original file line numberDiff line numberDiff line change
@@ -2490,6 +2490,7 @@ func TestSetupInitializesOnlyNilWriters(t *testing.T) {
24902490
}
24912491

24922492
func TestFlagAction(t *testing.T) {
2493+
now := time.Now().UTC().Truncate(time.Minute)
24932494
testCases := []struct {
24942495
name string
24952496
args []string
@@ -2578,8 +2579,8 @@ func TestFlagAction(t *testing.T) {
25782579
},
25792580
{
25802581
name: "flag_timestamp",
2581-
args: []string{"app", "--f_timestamp", "2022-05-01 02:26:20"},
2582-
exp: "2022-05-01T02:26:20Z ",
2582+
args: []string{"app", "--f_timestamp", now.Format(time.DateTime)},
2583+
exp: now.UTC().Format(time.RFC3339) + " ",
25832584
},
25842585
{
25852586
name: "flag_timestamp_error",
@@ -2738,12 +2739,14 @@ func TestFlagAction(t *testing.T) {
27382739
&TimestampFlag{
27392740
Name: "f_timestamp",
27402741
Config: TimestampConfig{
2741-
Layout: "2006-01-02 15:04:05",
2742+
Timezone: time.UTC,
2743+
Layouts: []string{time.DateTime},
27422744
},
27432745
Action: func(_ context.Context, cmd *Command, v time.Time) error {
27442746
if v.IsZero() {
27452747
return fmt.Errorf("zero timestamp")
27462748
}
2749+
27472750
_, err := cmd.Root().Writer.Write([]byte(v.Format(time.RFC3339) + " "))
27482751
return err
27492752
},

docs/v3/examples/timestamp-flag.md

+13-2
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import (
2828
func main() {
2929
cmd := &cli.Command{
3030
Flags: []cli.Flag{
31-
&cli.TimestampFlag{Name: "meeting", Config: cli.TimestampConfig{Layout: "2006-01-02T15:04:05"}},
31+
&cli.TimestampFlag{
32+
Name: "meeting",
33+
Config: cli.TimestampConfig{
34+
Layouts: []string{"2006-01-02T15:04:05"},
35+
},
36+
},
3237
},
3338
Action: func(ctx context.Context, cmd *cli.Command) error {
3439
fmt.Printf("%s", cmd.Timestamp("meeting").String())
@@ -54,7 +59,13 @@ change behavior, a default timezone can be provided with flag definition:
5459
```go
5560
cmd := &cli.Command{
5661
Flags: []cli.Flag{
57-
&cli.TimestampFlag{Name: "meeting", Config: cli.TimestampConfig{Layout: "2006-01-02T15:04:05", Timezone: time.Local}},
62+
&cli.TimestampFlag{
63+
Name: "meeting",
64+
Config: cli.TimestampConfig{
65+
Timezone: time.Local,
66+
AvailableLayouts: []string{"2006-01-02T15:04:05"},
67+
},
68+
},
5869
},
5970
}
6071
```

flag_test.go

+231-12
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@ package cli
22

33
import (
44
"context"
5+
"errors"
56
"flag"
67
"fmt"
78
"io"
89
"os"
910
"reflect"
11+
"regexp"
1012
"strings"
1113
"testing"
1214
"time"
@@ -2259,23 +2261,23 @@ func TestTimestamp_set(t *testing.T) {
22592261
ts := timestampValue{
22602262
timestamp: nil,
22612263
hasBeenSet: false,
2262-
layout: "Jan 2, 2006 at 3:04pm (MST)",
2264+
layouts: []string{"Jan 2, 2006 at 3:04pm (MST)"},
22632265
}
22642266

22652267
time1 := "Feb 3, 2013 at 7:54pm (PST)"
2266-
require.NoError(t, ts.Set(time1), "Failed to parse time %s with layout %s", time1, ts.layout)
2268+
require.NoError(t, ts.Set(time1), "Failed to parse time %s with layouts %v", time1, ts.layouts)
22672269
require.True(t, ts.hasBeenSet, "hasBeenSet is not true after setting a time")
22682270

22692271
ts.hasBeenSet = false
2270-
ts.layout = time.RFC3339
2272+
ts.layouts = []string{time.RFC3339}
22712273
time2 := "2006-01-02T15:04:05Z"
2272-
require.NoError(t, ts.Set(time2), "Failed to parse time %s with layout %s", time2, ts.layout)
2274+
require.NoError(t, ts.Set(time2), "Failed to parse time %s with layout %v", time2, ts.layouts)
22732275
require.True(t, ts.hasBeenSet, "hasBeenSet is not true after setting a time")
22742276
}
22752277

2276-
func TestTimestampFlagApply(t *testing.T) {
2278+
func TestTimestampFlagApply_SingleFormat(t *testing.T) {
22772279
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
2278-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.RFC3339}}
2280+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}}
22792281
set := flag.NewFlagSet("test", 0)
22802282
_ = fl.Apply(set)
22812283

@@ -2284,9 +2286,226 @@ func TestTimestampFlagApply(t *testing.T) {
22842286
assert.Equal(t, expectedResult, set.Lookup("time").Value.(flag.Getter).Get())
22852287
}
22862288

2289+
func TestTimestampFlagApply_MultipleFormats(t *testing.T) {
2290+
now := time.Now().UTC()
2291+
2292+
testCases := []struct {
2293+
caseName string
2294+
layoutsPrecisions map[string]time.Duration
2295+
expRes time.Time
2296+
expErrValidation func(err error) (validation error)
2297+
}{
2298+
{
2299+
caseName: "all_valid_layouts",
2300+
layoutsPrecisions: map[string]time.Duration{
2301+
time.RFC3339: time.Second,
2302+
time.DateTime: time.Second,
2303+
time.RFC1123: time.Second,
2304+
},
2305+
expRes: now.Truncate(time.Second),
2306+
},
2307+
{
2308+
caseName: "one_invalid_layout",
2309+
layoutsPrecisions: map[string]time.Duration{
2310+
time.RFC3339: time.Second,
2311+
time.DateTime: time.Second,
2312+
"foo": 0,
2313+
},
2314+
expRes: now.Truncate(time.Second),
2315+
},
2316+
{
2317+
caseName: "multiple_invalid_layouts",
2318+
layoutsPrecisions: map[string]time.Duration{
2319+
time.RFC3339: time.Second,
2320+
"foo": 0,
2321+
time.DateTime: time.Second,
2322+
"bar": 0,
2323+
},
2324+
expRes: now.Truncate(time.Second),
2325+
},
2326+
{
2327+
caseName: "all_invalid_layouts",
2328+
layoutsPrecisions: map[string]time.Duration{
2329+
"foo": 0,
2330+
"2024-08-07 74:01:82Z-100": 0,
2331+
"25:70": 0,
2332+
"": 0,
2333+
},
2334+
expErrValidation: func(err error) error {
2335+
if err == nil {
2336+
return errors.New("got nil err")
2337+
}
2338+
2339+
found := regexp.MustCompile(`(cannot parse ".+" as ".*")|(extra text: ".+")`).Match([]byte(err.Error()))
2340+
if !found {
2341+
return fmt.Errorf("given error does not satisfy pattern: %w", err)
2342+
}
2343+
2344+
return nil
2345+
},
2346+
},
2347+
{
2348+
caseName: "empty_layout",
2349+
layoutsPrecisions: map[string]time.Duration{
2350+
"": 0,
2351+
},
2352+
expErrValidation: func(err error) error {
2353+
if err == nil {
2354+
return errors.New("got nil err")
2355+
}
2356+
2357+
found := regexp.MustCompile(`extra text: ".+"`).Match([]byte(err.Error()))
2358+
if !found {
2359+
return fmt.Errorf("given error does not satisfy pattern: %w", err)
2360+
}
2361+
2362+
return nil
2363+
},
2364+
},
2365+
{
2366+
caseName: "nil_layouts_slice",
2367+
expErrValidation: func(err error) error {
2368+
if err == nil {
2369+
return errors.New("got nil err")
2370+
}
2371+
2372+
found := regexp.MustCompile(`got nil/empty layouts slice`).Match([]byte(err.Error()))
2373+
if !found {
2374+
return fmt.Errorf("given error does not satisfy pattern: %w", err)
2375+
}
2376+
2377+
return nil
2378+
},
2379+
},
2380+
{
2381+
caseName: "empty_layouts_slice",
2382+
layoutsPrecisions: map[string]time.Duration{},
2383+
expErrValidation: func(err error) error {
2384+
if err == nil {
2385+
return errors.New("got nil err")
2386+
}
2387+
2388+
found := regexp.MustCompile(`got nil/empty layouts slice`).Match([]byte(err.Error()))
2389+
if !found {
2390+
return fmt.Errorf("given error does not satisfy pattern: %w", err)
2391+
}
2392+
2393+
return nil
2394+
},
2395+
},
2396+
}
2397+
2398+
// TODO: replace with maps.Keys() (go >= ), lo.Keys() if acceptable
2399+
getKeys := func(m map[string]time.Duration) []string {
2400+
if m == nil {
2401+
return nil
2402+
}
2403+
2404+
keys := make([]string, 0, len(m))
2405+
for k := range m {
2406+
keys = append(keys, k)
2407+
}
2408+
return keys
2409+
}
2410+
2411+
for idx := range testCases {
2412+
testCase := testCases[idx]
2413+
t.Run(testCase.caseName, func(t *testing.T) {
2414+
// t.Parallel()
2415+
fl := TimestampFlag{
2416+
Name: "time",
2417+
Config: TimestampConfig{
2418+
Layouts: getKeys(testCase.layoutsPrecisions),
2419+
},
2420+
}
2421+
2422+
set := flag.NewFlagSet("test", 0)
2423+
_ = fl.Apply(set)
2424+
2425+
if len(testCase.layoutsPrecisions) == 0 {
2426+
err := set.Parse([]string{"--time", now.Format(time.RFC3339)})
2427+
if testCase.expErrValidation != nil {
2428+
assert.NoError(t, testCase.expErrValidation(err))
2429+
}
2430+
}
2431+
2432+
validLayouts := make([]string, 0, len(testCase.layoutsPrecisions))
2433+
invalidLayouts := make([]string, 0, len(testCase.layoutsPrecisions))
2434+
2435+
// TODO: replace with lo.Filter if acceptable
2436+
for layout, prec := range testCase.layoutsPrecisions {
2437+
v, err := time.Parse(layout, now.Format(layout))
2438+
if err != nil || prec == 0 || now.Truncate(prec).UnixNano() != v.Truncate(prec).UnixNano() {
2439+
invalidLayouts = append(invalidLayouts, layout)
2440+
continue
2441+
}
2442+
validLayouts = append(validLayouts, layout)
2443+
}
2444+
2445+
for _, layout := range validLayouts {
2446+
err := set.Parse([]string{"--time", now.Format(layout)})
2447+
assert.NoError(t, err)
2448+
if !testCase.expRes.IsZero() {
2449+
assert.Equal(t, testCase.expRes, set.Lookup("time").Value.(flag.Getter).Get())
2450+
}
2451+
}
2452+
2453+
for range invalidLayouts {
2454+
err := set.Parse([]string{"--time", now.Format(time.RFC3339)})
2455+
if testCase.expErrValidation != nil {
2456+
assert.NoError(t, testCase.expErrValidation(err))
2457+
}
2458+
}
2459+
})
2460+
}
2461+
}
2462+
2463+
func TestTimestampFlagApply_ShortenedLayouts(t *testing.T) {
2464+
now := time.Now().UTC()
2465+
2466+
shortenedLayoutsPrecisions := map[string]time.Duration{
2467+
time.Kitchen: time.Minute,
2468+
time.Stamp: time.Second,
2469+
time.StampMilli: time.Millisecond,
2470+
time.StampMicro: time.Microsecond,
2471+
time.StampNano: time.Nanosecond,
2472+
time.TimeOnly: time.Second,
2473+
"15:04": time.Minute,
2474+
}
2475+
2476+
// TODO: replace with maps.Keys() (go >= ), lo.Keys() if acceptable
2477+
getKeys := func(m map[string]time.Duration) []string {
2478+
if m == nil {
2479+
return nil
2480+
}
2481+
2482+
keys := make([]string, 0, len(m))
2483+
for k := range m {
2484+
keys = append(keys, k)
2485+
}
2486+
return keys
2487+
}
2488+
2489+
fl := TimestampFlag{
2490+
Name: "time",
2491+
Config: TimestampConfig{
2492+
Layouts: getKeys(shortenedLayoutsPrecisions),
2493+
},
2494+
}
2495+
2496+
set := flag.NewFlagSet("test", 0)
2497+
_ = fl.Apply(set)
2498+
2499+
for layout, prec := range shortenedLayoutsPrecisions {
2500+
err := set.Parse([]string{"--time", now.Format(layout)})
2501+
assert.NoError(t, err)
2502+
assert.Equal(t, now.Truncate(prec), set.Lookup("time").Value.(flag.Getter).Get())
2503+
}
2504+
}
2505+
22872506
func TestTimestampFlagApplyValue(t *testing.T) {
22882507
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
2289-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.RFC3339}, Value: expectedResult}
2508+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Value: expectedResult}
22902509
set := flag.NewFlagSet("test", 0)
22912510
_ = fl.Apply(set)
22922511

@@ -2296,7 +2515,7 @@ func TestTimestampFlagApplyValue(t *testing.T) {
22962515
}
22972516

22982517
func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) {
2299-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: "randomlayout"}}
2518+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{"randomlayout"}}}
23002519
set := flag.NewFlagSet("test", 0)
23012520
set.SetOutput(io.Discard)
23022521
_ = fl.Apply(set)
@@ -2306,7 +2525,7 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) {
23062525
}
23072526

23082527
func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
2309-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: "Jan 2, 2006 at 3:04pm (MST)"}}
2528+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{"Jan 2, 2006 at 3:04pm (MST)"}}}
23102529
set := flag.NewFlagSet("test", 0)
23112530
set.SetOutput(io.Discard)
23122531
_ = fl.Apply(set)
@@ -2318,7 +2537,7 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
23182537
func TestTimestampFlagApply_Timezoned(t *testing.T) {
23192538
pdt := time.FixedZone("PDT", -7*60*60)
23202539
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
2321-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.ANSIC, Timezone: pdt}}
2540+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.ANSIC}, Timezone: pdt}}
23222541
set := flag.NewFlagSet("test", 0)
23232542
_ = fl.Apply(set)
23242543

@@ -2519,7 +2738,7 @@ func TestFlagDefaultValueWithEnv(t *testing.T) {
25192738
},
25202739
{
25212740
name: "timestamp",
2522-
flag: &TimestampFlag{Name: "flag", Value: ts, Config: TimestampConfig{Layout: time.RFC3339}, Sources: EnvVars("tflag")},
2741+
flag: &TimestampFlag{Name: "flag", Value: ts, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Sources: EnvVars("tflag")},
25232742
toParse: []string{"--flag", "2006-11-02T15:04:05Z"},
25242743
expect: `--flag value (default: 2005-01-02 15:04:05 +0000 UTC)` + withEnvHint([]string{"tflag"}, ""),
25252744
environ: map[string]string{
@@ -2603,7 +2822,7 @@ func TestFlagValue(t *testing.T) {
26032822
func TestTimestampFlagApply_WithDestination(t *testing.T) {
26042823
var destination time.Time
26052824
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
2606-
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.RFC3339}, Destination: &destination}
2825+
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layouts: []string{time.RFC3339}}, Destination: &destination}
26072826
set := flag.NewFlagSet("test", 0)
26082827
_ = fl.Apply(set)
26092828

0 commit comments

Comments
 (0)