diff options
Diffstat (limited to 'internal/worktime')
| -rw-r--r-- | internal/worktime/db.go | 59 | ||||
| -rw-r--r-- | internal/worktime/db_test.go | 52 |
2 files changed, 110 insertions, 1 deletions
diff --git a/internal/worktime/db.go b/internal/worktime/db.go index ad87c4d..e6b06f4 100644 --- a/internal/worktime/db.go +++ b/internal/worktime/db.go @@ -4,9 +4,11 @@ import ( "encoding/json" "errors" "fmt" + "math" "os" "path/filepath" "sort" + "strconv" "strings" ) @@ -23,6 +25,51 @@ type Entry struct { Descr string `json:"descr,omitempty"` } +// UnmarshalJSON supports legacy value encodings where "value" can be int or float. +func (e *Entry) UnmarshalJSON(data []byte) error { + type entryAlias Entry + aux := struct { + entryAlias + Value json.RawMessage `json:"value"` + }{} + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + *e = Entry(aux.entryAlias) + e.Value = 0 + + raw := strings.TrimSpace(string(aux.Value)) + if raw == "" || raw == "null" { + return nil + } + + var intValue int64 + if err := json.Unmarshal(aux.Value, &intValue); err == nil { + e.Value = intValue + return nil + } + + var floatValue float64 + if err := json.Unmarshal(aux.Value, &floatValue); err == nil { + e.Value = int64(math.Round(floatValue)) + return nil + } + + var stringValue string + if err := json.Unmarshal(aux.Value, &stringValue); err == nil { + parsed, parseErr := strconv.ParseFloat(strings.TrimSpace(stringValue), 64) + if parseErr != nil { + return fmt.Errorf("parse string value %q: %w", stringValue, parseErr) + } + e.Value = int64(math.Round(parsed)) + return nil + } + + return fmt.Errorf("unsupported value encoding %s", raw) +} + // Database is the on-disk JSON structure used by worktime. type Database struct { Entries map[string][]Entry `json:"entries"` @@ -45,7 +92,12 @@ func LoadAll(dbDir string) ([]Entry, error) { if err != nil { return nil, err } - for _, hostEntries := range db.Entries { + for host, hostEntries := range db.Entries { + for idx := range hostEntries { + if strings.TrimSpace(hostEntries[idx].Source) == "" { + hostEntries[idx].Source = host + } + } entries = append(entries, hostEntries...) } } @@ -76,6 +128,11 @@ func LoadHost(dbDir, hostname string) (Database, error) { if _, ok := db.Entries[host]; !ok { db.Entries[host] = []Entry{} } + for idx := range db.Entries[host] { + if strings.TrimSpace(db.Entries[host][idx].Source) == "" { + db.Entries[host][idx].Source = host + } + } sortEntries(db.Entries[host]) return db, nil } diff --git a/internal/worktime/db_test.go b/internal/worktime/db_test.go index d011a9e..f599fa1 100644 --- a/internal/worktime/db_test.go +++ b/internal/worktime/db_test.go @@ -129,6 +129,32 @@ func TestLoadAllMergesAndSortsEntries(t *testing.T) { } } +func TestLoadAllBackfillsMissingSourceFromHost(t *testing.T) { + dbDir := t.TempDir() + dbFile := filepath.Join(dbDir, "db.host-a.json") + content := `{ + "entries": { + "host-a": [ + {"action":"login","what":"work","epoch":10,"human":"h1"} + ] + } +}` + if err := os.WriteFile(dbFile, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + entries, err := LoadAll(dbDir) + if err != nil { + t.Fatalf("LoadAll() error = %v", err) + } + if len(entries) != 1 { + t.Fatalf("entries len = %d, want 1", len(entries)) + } + if entries[0].Source != "host-a" { + t.Fatalf("entries[0].Source = %q, want host-a", entries[0].Source) + } +} + func TestLoadAllOnMissingDirectoryReturnsEmptySlice(t *testing.T) { dbDir := filepath.Join(t.TempDir(), "does-not-exist") @@ -159,6 +185,32 @@ func TestLoadHostInvalidJSON(t *testing.T) { } } +func TestLoadAllAcceptsFloatValueEncoding(t *testing.T) { + dbDir := t.TempDir() + dbFile := filepath.Join(dbDir, "db.host-a.json") + content := `{ + "entries": { + "host-a": [ + {"action":"add","what":"work","epoch":10,"source":"host-a","human":"h1","value":31680.000000000004} + ] + } +}` + if err := os.WriteFile(dbFile, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + + entries, err := LoadAll(dbDir) + if err != nil { + t.Fatalf("LoadAll() error = %v", err) + } + if len(entries) != 1 { + t.Fatalf("entries len = %d, want 1", len(entries)) + } + if entries[0].Value != 31680 { + t.Fatalf("entries[0].Value = %d, want 31680", entries[0].Value) + } +} + func TestLoadAllInvalidJSON(t *testing.T) { dbDir := t.TempDir() badFile := filepath.Join(dbDir, "db.host-a.json") |
