Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions pkg/config/timestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"fmt"
"strconv"
"time"
)

// Timestamp represents a Unix timestamp (seconds) that marshals to/from a string.
// Supported input formats:
// - RFC3339 (e.g. "2025-06-15T12:30:45Z")
// - Go default, nanoseconds truncated (e.g. "2006-01-02 15:04:05 -0700 MST" or "2006-01-02 15:04:05.123456789 -0700 MST")
// - Integer Unix timestamp (seconds)
type Timestamp int64

const defaultFormat = "2006-01-02 15:04:05 -0700 MST"

func (t Timestamp) String() string {
return time.Unix(int64(t), 0).UTC().Format(defaultFormat)
}

func (t Timestamp) MarshalText() ([]byte, error) {
return []byte(t.String()), nil
}

func (t *Timestamp) UnmarshalText(b []byte) error {
s := string(b)
if parsed, err := time.Parse(defaultFormat, s); err == nil {
*t = Timestamp(parsed.Unix())
return nil
}
if parsed, err := time.Parse(time.RFC3339, s); err == nil {
*t = Timestamp(parsed.Unix())
return nil
}
v, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse timestamp %q: %w", s, err)
}
*t = Timestamp(v)
return nil
}

func ParseTimestamp(s string) (Timestamp, error) {
var t Timestamp
err := t.UnmarshalText([]byte(s))
return t, err
}
Comment thread
bolekk marked this conversation as resolved.
48 changes: 48 additions & 0 deletions pkg/config/timestamp_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package config

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestParseTimestamp(t *testing.T) {
ref := time.Date(2025, 6, 15, 12, 30, 45, 0, time.UTC)

tests := []struct {
name string
input string
want Timestamp
}{
{
name: "RFC3339",
input: "2025-06-15T12:30:45Z",
want: Timestamp(ref.Unix()),
},
{
name: "Go default without nanoseconds",
input: "2025-06-15 12:30:45 +0000 UTC",
want: Timestamp(ref.Unix()),
},
{
name: "Go default - nanoseconds truncated",
input: "2025-06-15 12:30:45.123456789 +0000 UTC",
want: Timestamp(ref.Unix()),
},
{
name: "Unix integer",
input: "1749990645",
want: Timestamp(ref.Unix()),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := ParseTimestamp(tt.input)
require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}
2 changes: 1 addition & 1 deletion pkg/settings/cresettings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ type Workflows struct {
ConfidentialHTTP confidentialHTTP
Secrets secrets

FeatureMultiTriggerExecutionIDsActiveAt Setting[time.Time]
FeatureMultiTriggerExecutionIDsActiveAt Setting[config.Timestamp]
}

type cronTrigger struct {
Expand Down
2 changes: 1 addition & 1 deletion pkg/settings/cresettings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ func TestSchema_Unmarshal(t *testing.T) {
assert.Equal(t, 5, cfg.PerWorkflow.Secrets.CallLimit.DefaultValue)
assert.Equal(t, uint64(500000), cfg.PerWorkflow.ChainWrite.EVM.TransactionGasLimit.DefaultValue)
assert.Equal(t, 3, cfg.PerWorkflow.ChainRead.CallLimit.DefaultValue)
assert.Equal(t, time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC), cfg.PerWorkflow.FeatureMultiTriggerExecutionIDsActiveAt.DefaultValue)
assert.Equal(t, config.Timestamp(time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC).Unix()), cfg.PerWorkflow.FeatureMultiTriggerExecutionIDsActiveAt.DefaultValue)
}

func TestDefaultGetter(t *testing.T) {
Expand Down
9 changes: 2 additions & 7 deletions pkg/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,13 +116,8 @@ func Duration(defaultValue time.Duration) Setting[time.Duration] {
return s
}

func Time(defaultValue time.Time) Setting[time.Time] {
return NewSetting(defaultValue, func(s string) (time.Time, error) {
if t, err := time.Parse(time.RFC3339Nano, s); err == nil {
return t, nil
}
return time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", s) // Go default format
})
func Time(defaultValue time.Time) Setting[config.Timestamp] {
return NewSetting(config.Timestamp(defaultValue.Unix()), config.ParseTimestamp)
Comment thread
bolekk marked this conversation as resolved.
}

func URL(defaultValue *url.URL) Setting[*url.URL] {
Expand Down
18 changes: 7 additions & 11 deletions pkg/settings/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,25 @@ func TestTime(t *testing.T) {
t.Run("parse RFC3339", func(t *testing.T) {
got, err := s.Parse("2025-06-15T12:30:00Z")
require.NoError(t, err)
assert.Equal(t, time.Date(2025, 6, 15, 12, 30, 0, 0, time.UTC), got)
assert.Equal(t, config.Timestamp(time.Date(2025, 6, 15, 12, 30, 0, 0, time.UTC).Unix()), got)
})

t.Run("parse RFC3339 with nanoseconds", func(t *testing.T) {
got, err := s.Parse("2025-06-15T12:30:00.123456789Z")
require.NoError(t, err)
assert.Equal(t, time.Date(2025, 6, 15, 12, 30, 0, 123456789, time.UTC), got)
assert.Equal(t, config.Timestamp(time.Date(2025, 6, 15, 12, 30, 0, 0, time.UTC).Unix()), got)
})

t.Run("parse Go default format", func(t *testing.T) {
got, err := s.Parse("2025-06-15 00:00:00 +0000 UTC")
require.NoError(t, err)
assert.Equal(t, time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC), got)
assert.Equal(t, config.Timestamp(time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC).Unix()), got)
})

t.Run("parse Go default format with offset", func(t *testing.T) {
got, err := s.Parse("2025-06-15 12:30:00 +0530 IST")
got, err := s.Parse("2025-06-15 12:30:00.89 +0530 IST")
require.NoError(t, err)
assert.Equal(t, 2025, got.Year())
assert.Equal(t, time.Month(6), got.Month())
assert.Equal(t, 15, got.Day())
assert.Equal(t, 12, got.Hour())
assert.Equal(t, 30, got.Minute())
assert.Equal(t, time.Date(2025, 6, 15, 7, 0, 0, 0, time.UTC), time.Unix(int64(got), 0).UTC())
})

t.Run("MarshalText round-trip", func(t *testing.T) {
Expand All @@ -57,9 +53,9 @@ func TestTime(t *testing.T) {
})

t.Run("UnmarshalText", func(t *testing.T) {
var s2 Setting[time.Time]
var s2 Setting[config.Timestamp]
s2.Parse = s.Parse
require.NoError(t, s2.UnmarshalText([]byte("2100-01-01T00:00:00Z")))
require.NoError(t, s2.UnmarshalText([]byte("2100-01-01 00:00:00 +0000 UTC")))
assert.Equal(t, s.DefaultValue, s2.DefaultValue)
})

Expand Down
Loading