internal/store/timestamp.go
1
package store
3
import (
4
"fmt"
5
"time"
7
yaml "go.yaml.in/yaml/v3"
8
)
10
// Timestamp is an instant in UTC. A v1 record carried a calendar date; both
11
// forms are read and only the instant is written back, so a reader comparing
12
// two records never has to know which generation wrote them.
13
type Timestamp struct{ time.Time }
15
const (
16
timestampLayout = time.RFC3339
17
dateLayout = "2006-01-02"
18
)
20
func Now() Timestamp { return Timestamp{time.Now().UTC().Truncate(time.Second)} }
22
func ParseTimestamp(text string) (Timestamp, error) {
23
if instant, err := time.Parse(timestampLayout, text); err == nil {
24
return Timestamp{instant.UTC()}, nil
25
}
26
if day, err := time.Parse(dateLayout, text); err == nil {
27
return Timestamp{day.UTC()}, nil
28
}
29
return Timestamp{}, fmt.Errorf("%q is neither an RFC3339 instant nor a %s date", text, dateLayout)
30
}
32
func (t Timestamp) MarshalYAML() (any, error) { return t.UTC().Format(timestampLayout), nil }
34
// UnmarshalYAML reads the node rather than a decoded value because an
35
// unquoted 2026-08-05 resolves to !!timestamp, which will not decode into a
36
// string at all. Reading Value sidesteps tag resolution entirely.
37
func (t *Timestamp) UnmarshalYAML(node *yaml.Node) error {
38
if node.Kind != yaml.ScalarNode {
39
return fmt.Errorf("a timestamp is a single value, got %s", node.Tag)
40
}
41
parsed, err := ParseTimestamp(node.Value)
42
if err != nil {
43
return err
44
}
45
*t = parsed
46
return nil
47
}