feat: add timezone support with automatic timestamp conversion
Add GetTimezone() API method and WithTimezone() client option. When enabled, the client lazily fetches the server timezone on first API call and converts all Timestamp fields in responses using reflection-based struct walking.
This commit is contained in:
parent
8063341150
commit
1fba43cf90
6 changed files with 339 additions and 3 deletions
81
timezone.go
Normal file
81
timezone.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package kanboard
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GetTimezone returns the server's configured timezone string (e.g., "UTC", "Europe/Berlin").
|
||||
func (c *Client) GetTimezone(ctx context.Context) (string, error) {
|
||||
var tz string
|
||||
if err := c.call(ctx, "getTimezone", nil, &tz); err != nil {
|
||||
return "", fmt.Errorf("getTimezone: %w", err)
|
||||
}
|
||||
return tz, nil
|
||||
}
|
||||
|
||||
// loadTimezone fetches and caches the timezone location from the server.
|
||||
func (c *Client) loadTimezone(ctx context.Context) error {
|
||||
tz, err := c.GetTimezone(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
loc, err := time.LoadLocation(tz)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid timezone %q: %w", tz, err)
|
||||
}
|
||||
c.timezone = loc
|
||||
return nil
|
||||
}
|
||||
|
||||
// ensureTimezone loads the timezone if tzEnabled and not yet loaded.
|
||||
func (c *Client) ensureTimezone(ctx context.Context) error {
|
||||
if !c.tzEnabled {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
c.tzOnce.Do(func() {
|
||||
err = c.loadTimezone(ctx)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// convertTimestamps converts all Timestamp fields in v to the client's timezone.
|
||||
// v must be a pointer. Handles structs, pointers to structs, and slices of structs.
|
||||
func (c *Client) convertTimestamps(v any) {
|
||||
if c.timezone == nil {
|
||||
return
|
||||
}
|
||||
rv := reflect.ValueOf(v)
|
||||
c.walkAndConvert(rv)
|
||||
}
|
||||
|
||||
var timestampType = reflect.TypeOf(Timestamp{})
|
||||
|
||||
func (c *Client) walkAndConvert(rv reflect.Value) {
|
||||
switch rv.Kind() {
|
||||
case reflect.Ptr:
|
||||
if !rv.IsNil() {
|
||||
c.walkAndConvert(rv.Elem())
|
||||
}
|
||||
case reflect.Struct:
|
||||
if rv.Type() == timestampType {
|
||||
if rv.CanSet() {
|
||||
ts := rv.Addr().Interface().(*Timestamp)
|
||||
if !ts.IsZero() {
|
||||
ts.Time = ts.Time.In(c.timezone)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
for i := 0; i < rv.NumField(); i++ {
|
||||
c.walkAndConvert(rv.Field(i))
|
||||
}
|
||||
case reflect.Slice:
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
c.walkAndConvert(rv.Index(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue