Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat:(issue_1958) Add support for multiple layouts to TimestampFlag #1959

Merged
merged 19 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion args_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestArgumentsSubcommand(t *testing.T) {
Max: 1,
Destination: &tval,
Config: TimestampConfig{
Layout: time.RFC3339,
AvailableLayouts: []string{time.RFC3339},
},
},
&StringArg{
Expand Down
9 changes: 6 additions & 3 deletions command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,7 @@ func TestSetupInitializesOnlyNilWriters(t *testing.T) {
}

func TestFlagAction(t *testing.T) {
now := time.Now().UTC().Truncate(time.Minute)
testCases := []struct {
name string
args []string
Expand Down Expand Up @@ -2578,8 +2579,8 @@ func TestFlagAction(t *testing.T) {
},
{
name: "flag_timestamp",
args: []string{"app", "--f_timestamp", "2022-05-01 02:26:20"},
exp: "2022-05-01T02:26:20Z ",
args: []string{"app", "--f_timestamp", now.Format(time.DateTime)},
exp: now.UTC().Format(time.RFC3339) + " ",
},
{
name: "flag_timestamp_error",
Expand Down Expand Up @@ -2738,12 +2739,14 @@ func TestFlagAction(t *testing.T) {
&TimestampFlag{
Name: "f_timestamp",
Config: TimestampConfig{
Layout: "2006-01-02 15:04:05",
Timezone: time.UTC,
AvailableLayouts: []string{time.DateTime},
},
Action: func(_ context.Context, cmd *Command, v time.Time) error {
if v.IsZero() {
return fmt.Errorf("zero timestamp")
}

_, err := cmd.Root().Writer.Write([]byte(v.Format(time.RFC3339) + " "))
return err
},
Expand Down
15 changes: 13 additions & 2 deletions docs/v3/examples/timestamp-flag.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ import (
func main() {
cmd := &cli.Command{
Flags: []cli.Flag{
&cli.TimestampFlag{Name: "meeting", Config: cli.TimestampConfig{Layout: "2006-01-02T15:04:05"}},
&cli.TimestampFlag{
Name: "meeting",
Config: cli.TimestampConfig{
AvailableLayouts: []string{"2006-01-02T15:04:05"},
},
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
fmt.Printf("%s", cmd.Timestamp("meeting").String())
Expand All @@ -54,7 +59,13 @@ change behavior, a default timezone can be provided with flag definition:
```go
cmd := &cli.Command{
Flags: []cli.Flag{
&cli.TimestampFlag{Name: "meeting", Config: cli.TimestampConfig{Layout: "2006-01-02T15:04:05", Timezone: time.Local}},
&cli.TimestampFlag{
Name: "meeting",
Config: cli.TimestampConfig{
Timezone: time.Local,
AvailableLayouts: []string{"2006-01-02T15:04:05"},
},
},
},
}
```
Expand Down
56 changes: 42 additions & 14 deletions flag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2257,25 +2257,25 @@ func TestStringMap_Serialized_Set(t *testing.T) {

func TestTimestamp_set(t *testing.T) {
ts := timestampValue{
timestamp: nil,
hasBeenSet: false,
layout: "Jan 2, 2006 at 3:04pm (MST)",
timestamp: nil,
hasBeenSet: false,
availableLayouts: []string{"Jan 2, 2006 at 3:04pm (MST)"},
}

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

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

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

Expand All @@ -2284,9 +2284,37 @@ func TestTimestampFlagApply(t *testing.T) {
assert.Equal(t, expectedResult, set.Lookup("time").Value.(flag.Getter).Get())
}

func TestTimestampFlagApply_MultipleFormats(t *testing.T) {
fl := TimestampFlag{
Name: "time",
Aliases: []string{"t"},
Config: TimestampConfig{
Timezone: time.Local,
AvailableLayouts: []string{
time.DateTime,
time.TimeOnly,
time.Kitchen,
},
},
}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)

now := time.Now()
for timeStr, expectedRes := range map[string]time.Time{
now.Format(time.DateTime): now.Truncate(time.Second),
now.Format(time.TimeOnly): now.Truncate(time.Second),
now.Format(time.Kitchen): now.Truncate(time.Minute),
} {
err := set.Parse([]string{"--time", timeStr})
assert.NoError(t, err)
assert.Equal(t, expectedRes, set.Lookup("time").Value.(flag.Getter).Get())
}
}
bartekpacia marked this conversation as resolved.
Show resolved Hide resolved

func TestTimestampFlagApplyValue(t *testing.T) {
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.RFC3339}, Value: expectedResult}
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{AvailableLayouts: []string{time.RFC3339}}, Value: expectedResult}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)

Expand All @@ -2296,7 +2324,7 @@ func TestTimestampFlagApplyValue(t *testing.T) {
}

func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) {
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: "randomlayout"}}
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{AvailableLayouts: []string{"randomlayout"}}}
set := flag.NewFlagSet("test", 0)
set.SetOutput(io.Discard)
_ = fl.Apply(set)
Expand All @@ -2306,7 +2334,7 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Layout(t *testing.T) {
}

func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: "Jan 2, 2006 at 3:04pm (MST)"}}
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{AvailableLayouts: []string{"Jan 2, 2006 at 3:04pm (MST)"}}}
set := flag.NewFlagSet("test", 0)
set.SetOutput(io.Discard)
_ = fl.Apply(set)
Expand All @@ -2318,7 +2346,7 @@ func TestTimestampFlagApply_Fail_Parse_Wrong_Time(t *testing.T) {
func TestTimestampFlagApply_Timezoned(t *testing.T) {
pdt := time.FixedZone("PDT", -7*60*60)
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.ANSIC, Timezone: pdt}}
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{AvailableLayouts: []string{time.ANSIC}, Timezone: pdt}}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)

Expand Down Expand Up @@ -2519,7 +2547,7 @@ func TestFlagDefaultValueWithEnv(t *testing.T) {
},
{
name: "timestamp",
flag: &TimestampFlag{Name: "flag", Value: ts, Config: TimestampConfig{Layout: time.RFC3339}, Sources: EnvVars("tflag")},
flag: &TimestampFlag{Name: "flag", Value: ts, Config: TimestampConfig{AvailableLayouts: []string{time.RFC3339}}, Sources: EnvVars("tflag")},
toParse: []string{"--flag", "2006-11-02T15:04:05Z"},
expect: `--flag value (default: 2005-01-02 15:04:05 +0000 UTC)` + withEnvHint([]string{"tflag"}, ""),
environ: map[string]string{
Expand Down Expand Up @@ -2603,7 +2631,7 @@ func TestFlagValue(t *testing.T) {
func TestTimestampFlagApply_WithDestination(t *testing.T) {
var destination time.Time
expectedResult, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05Z")
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{Layout: time.RFC3339}, Destination: &destination}
fl := TimestampFlag{Name: "time", Aliases: []string{"t"}, Config: TimestampConfig{AvailableLayouts: []string{time.RFC3339}}, Destination: &destination}
set := flag.NewFlagSet("test", 0)
_ = fl.Apply(set)

Expand Down
61 changes: 48 additions & 13 deletions flag_timestamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ type TimestampFlag = FlagBase[time.Time, TimestampConfig, timestampValue]

// TimestampConfig defines the config for timestamp flags
type TimestampConfig struct {
Timezone *time.Location
Layout string
Timezone *time.Location
AvailableLayouts []string
horockey marked this conversation as resolved.
Show resolved Hide resolved
}

// timestampValue wrap to satisfy golang's flag interface.
type timestampValue struct {
timestamp *time.Time
hasBeenSet bool
layout string
location *time.Location
timestamp *time.Time
hasBeenSet bool
availableLayouts []string
horockey marked this conversation as resolved.
Show resolved Hide resolved
location *time.Location
}

var _ ValueCreator[time.Time, TimestampConfig] = timestampValue{}
Expand All @@ -28,9 +28,9 @@ var _ ValueCreator[time.Time, TimestampConfig] = timestampValue{}
func (t timestampValue) Create(val time.Time, p *time.Time, c TimestampConfig) Value {
*p = val
return &timestampValue{
timestamp: p,
layout: c.Layout,
location: c.Timezone,
timestamp: p,
availableLayouts: c.AvailableLayouts,
location: c.Timezone,
}
}

Expand All @@ -53,16 +53,51 @@ func (t *timestampValue) Set(value string) error {
var timestamp time.Time
var err error

if t.location != nil {
timestamp, err = time.ParseInLocation(t.layout, value, t.location)
} else {
timestamp, err = time.Parse(t.layout, value)
if t.location == nil {
t.location = time.UTC
}

for _, layout := range t.availableLayouts {
var locErr error

timestamp, locErr = time.ParseInLocation(layout, value, t.location)
// TODO: replace with errors.Join() after upgrading to go 1.20 or newer
// OR use external error wrapping, if acceptable
if locErr != nil {
if err == nil {
horockey marked this conversation as resolved.
Show resolved Hide resolved
err = locErr
continue
}

err = fmt.Errorf("%v\n%v", err, locErr)
continue
}

err = nil
break
}

if err != nil {
return err
}

defaultTS, _ := time.ParseInLocation(time.TimeOnly, time.TimeOnly, timestamp.Location())

// If format is missing date, set it explicitly to current
if timestamp.Truncate(time.Hour*24).UnixNano() == defaultTS.Truncate(time.Hour*24).UnixNano() {
n := time.Now()
timestamp = time.Date(
n.Year(),
n.Month(),
n.Day(),
timestamp.Hour(),
timestamp.Minute(),
timestamp.Second(),
timestamp.Nanosecond(),
timestamp.Location(),
)
}

if t.timestamp != nil {
*t.timestamp = timestamp
}
Expand Down
4 changes: 2 additions & 2 deletions godoc-current.txt
Original file line number Diff line number Diff line change
Expand Up @@ -940,8 +940,8 @@ type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]

type TimestampConfig struct {
Timezone *time.Location
Layout string
Timezone *time.Location
AvailableLayouts []string
}
TimestampConfig defines the config for timestamp flags

Expand Down
4 changes: 2 additions & 2 deletions testdata/godoc-v3.x.txt
Original file line number Diff line number Diff line change
Expand Up @@ -940,8 +940,8 @@ type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]

type TimestampConfig struct {
Timezone *time.Location
Layout string
Timezone *time.Location
AvailableLayouts []string
}
TimestampConfig defines the config for timestamp flags

Expand Down
Loading