logbook/main.go

484 lines
11 KiB
Go

package logbook
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"time"
"git.wow.st/gmp/logbook/parser"
bolt "go.etcd.io/bbolt"
)
// --- Bucket names ---
var (
bucketSets = []byte("sets")
bucketSetGroups = []byte("setgroups")
bucketWorkouts = []byte("workouts")
)
// WorkoutRepository ---
type WorkoutRepository struct {
db *bolt.DB
}
func NewRepositories(dbPath string) (*WorkoutRepository, *MeasurementRepository, error) {
db, err := bolt.Open(dbPath, 0600, nil)
if err != nil {
return nil, nil, err
}
err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(bucketSets)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(bucketSetGroups)
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists(bucketWorkouts)
if err != nil {
return err
}
return nil
})
if err != nil {
return nil, nil, err
}
return &WorkoutRepository{db: db}, &MeasurementRepository{db: db}, nil
}
func (wr *WorkoutRepository) Close() {
wr.db.Close()
}
// --- Helper functions ---
func getStruct(b *bolt.Bucket, id string, v interface{}) error {
if b == nil {
return errors.New("bucket does not exist")
}
data := b.Get([]byte(id))
if data == nil {
return errors.New("not found")
}
return json.Unmarshal(data, v)
}
func putStruct(b *bolt.Bucket, id string, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
return b.Put([]byte(id), data)
}
func deleteStruct(b *bolt.Bucket, id string) error {
if b == nil {
return nil
}
return b.Delete([]byte(id))
}
// --- ID generation (RFC3339 date + context + random suffix) ---
func generateDateID(date time.Time, suffix string) string {
base := date.UTC().Format("2006-01-02T150405Z07")
if suffix != "" {
base += "-" + sanitizeForID(suffix)
}
randBytes := make([]byte, 4)
_, _ = rand.Read(randBytes)
return fmt.Sprintf("%s-%s", base, hex.EncodeToString(randBytes))
}
func sanitizeForID(s string) string {
out := ""
for _, ch := range s {
if ch == ' ' || ch == '/' || ch == '\\' {
out += "_"
} else {
out += string(ch)
}
}
return out
}
// --- Data Structures (for DB storage) ---
type Set struct {
ID string
Weight float64
Reps int
Note string
Time time.Time
RIR int
}
type SetGroup struct {
ID string
Exercise string
Type string
Note string
PlannedSetIDs []string
ActualSetIDs []string
}
type Workout struct {
ID string
Date time.Time
Type string
Note string
SetGroupIDs []string
}
// --- AddWorkoutsFromIR (with bucket creation) ---
func (r *WorkoutRepository) AddWorkoutsFromIR(irWorkouts []parser.Workout) error {
db := r.db
if db == nil {
return fmt.Errorf("Workout Repository is not initialized.")
}
return db.Update(func(tx *bolt.Tx) error {
// Ensure buckets exist
setsB, err := tx.CreateBucketIfNotExists(bucketSets)
if err != nil {
return err
}
setGroupsB, err := tx.CreateBucketIfNotExists(bucketSetGroups)
if err != nil {
return err
}
workoutsB, err := tx.CreateBucketIfNotExists(bucketWorkouts)
if err != nil {
return err
}
for _, irW := range irWorkouts {
workoutID := generateDateID(irW.Date, "")
w := &Workout{
ID: workoutID,
Date: irW.Date,
Type: irW.Type,
Note: irW.Note,
SetGroupIDs: nil,
}
for gidx, irG := range irW.SetGroups {
setGroupID := generateDateID(irW.Date, fmt.Sprintf("%s-%d", irG.Exercise, gidx))
sg := &SetGroup{
ID: setGroupID,
Exercise: irG.Exercise,
Type: irG.Type,
Note: irG.Note,
PlannedSetIDs: nil,
ActualSetIDs: nil,
}
// Add Planned Sets
for i, irSet := range irG.PlannedSets {
setID := generateDateID(irW.Date, fmt.Sprintf("%s-planned-%d", irG.Exercise, i))
set := &Set{
ID: setID,
Weight: irSet.Weight,
Reps: irSet.Reps,
Note: irSet.Note,
Time: irSet.Time,
RIR: irSet.RIR,
}
if err := putStruct(setsB, set.ID, set); err != nil {
return fmt.Errorf("failed to store planned set: %w", err)
}
sg.PlannedSetIDs = append(sg.PlannedSetIDs, set.ID)
}
// Add Actual Sets
for i, irSet := range irG.ActualSets {
setID := generateDateID(irW.Date, fmt.Sprintf("%s-actual-%d", irG.Exercise, i))
set := &Set{
ID: setID,
Weight: irSet.Weight,
Reps: irSet.Reps,
Note: irSet.Note,
Time: irSet.Time,
RIR: irSet.RIR,
}
if err := putStruct(setsB, set.ID, set); err != nil {
return fmt.Errorf("failed to store actual set: %w", err)
}
sg.ActualSetIDs = append(sg.ActualSetIDs, set.ID)
}
// Store SetGroup
if err := putStruct(setGroupsB, sg.ID, sg); err != nil {
return fmt.Errorf("failed to store set group: %w", err)
}
w.SetGroupIDs = append(w.SetGroupIDs, sg.ID)
}
// Store Workout
if err := putStruct(workoutsB, w.ID, w); err != nil {
return fmt.Errorf("failed to store workout: %w", err)
}
}
return nil
})
}
// --- GET METHODS ---
func (r *WorkoutRepository) GetWorkout(id string) (*parser.Workout, error) {
db := r.db
var w Workout
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketWorkouts)
if b == nil {
return errors.New("workouts bucket does not exist")
}
return getStruct(b, id, &w)
})
if err != nil {
return nil, err
}
ir := &parser.Workout{
Date: w.Date,
Type: w.Type,
Note: w.Note,
SetGroups: nil,
}
for _, sgid := range w.SetGroupIDs {
sg, err := r.GetSetGroup(sgid)
if err != nil {
return nil, fmt.Errorf("get setgroup: %w", err)
}
ir.SetGroups = append(ir.SetGroups, *sg)
}
return ir, nil
}
func (r *WorkoutRepository) GetSetGroup(id string) (*parser.SetGroup, error) {
db := r.db
var sg SetGroup
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketSetGroups)
if b == nil {
return errors.New("setgroups bucket does not exist")
}
return getStruct(b, id, &sg)
})
if err != nil {
return nil, err
}
ir := &parser.SetGroup{
Exercise: sg.Exercise,
Type: sg.Type,
Note: sg.Note,
PlannedSets: nil,
ActualSets: nil,
}
for _, setid := range sg.PlannedSetIDs {
s, err := r.GetSet(setid)
if err != nil {
return nil, fmt.Errorf("get planned set: %w", err)
}
ir.PlannedSets = append(ir.PlannedSets, *s)
}
for _, setid := range sg.ActualSetIDs {
s, err := r.GetSet(setid)
if err != nil {
return nil, fmt.Errorf("get actual set: %w", err)
}
ir.ActualSets = append(ir.ActualSets, *s)
}
return ir, nil
}
func (r *WorkoutRepository) GetSet(id string) (*parser.Set, error) {
db := r.db
var s Set
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketSets)
if b == nil {
return errors.New("sets bucket does not exist")
}
return getStruct(b, id, &s)
})
if err != nil {
return nil, err
}
return &parser.Set{
Weight: s.Weight,
Reps: s.Reps,
Note: s.Note,
Time: s.Time,
RIR: s.RIR,
}, nil
}
// --- FIND METHODS ---
// FindWorkouts returns a slice of workouts in parser.Workout format
// (intermediate representation) as well as a slice of workout IDs.
func (r *WorkoutRepository) FindWorkouts(match func(*parser.Workout) bool) ([]*parser.Workout, []string, error) {
db := r.db
var ws []*parser.Workout
var ids []string
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketWorkouts)
if b == nil {
// No workouts bucket yet; return empty list, no error
return nil
}
return b.ForEach(func(k, v []byte) error {
var w Workout
if err := json.Unmarshal(v, &w); err != nil {
return err
}
ir := &parser.Workout{
Date: w.Date,
Type: w.Type,
Note: w.Note,
SetGroups: nil,
}
for _, sgid := range w.SetGroupIDs {
sg, err := r.GetSetGroup(sgid)
if err != nil {
return err
}
ir.SetGroups = append(ir.SetGroups, *sg)
}
if match(ir) {
ws = append(ws, ir)
ids = append(ids, w.ID)
}
return nil
})
})
return ws, ids, err
}
func (r *WorkoutRepository) FindSetGroups(match func(*parser.SetGroup) bool) ([]*parser.SetGroup, error) {
db := r.db
var out []*parser.SetGroup
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketSetGroups)
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var sg SetGroup
if err := json.Unmarshal(v, &sg); err != nil {
return err
}
ir := &parser.SetGroup{
Exercise: sg.Exercise,
Type: sg.Type,
Note: sg.Note,
PlannedSets: nil,
ActualSets: nil,
}
for _, setid := range sg.PlannedSetIDs {
s, err := r.GetSet(setid)
if err != nil {
return err
}
ir.PlannedSets = append(ir.PlannedSets, *s)
}
for _, setid := range sg.ActualSetIDs {
s, err := r.GetSet(setid)
if err != nil {
return err
}
ir.ActualSets = append(ir.ActualSets, *s)
}
if match(ir) {
out = append(out, ir)
}
return nil
})
})
return out, err
}
func (r *WorkoutRepository) FindSets(match func(*parser.Set) bool) ([]*parser.Set, error) {
db := r.db
var out []*parser.Set
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket(bucketSets)
if b == nil {
return nil
}
return b.ForEach(func(k, v []byte) error {
var s Set
if err := json.Unmarshal(v, &s); err != nil {
return err
}
ir := &parser.Set{
Weight: s.Weight,
Reps: s.Reps,
Note: s.Note,
Time: s.Time,
}
if match(ir) {
out = append(out, ir)
}
return nil
})
})
return out, err
}
// --- UPDATE METHODS (stubs: see previous answer for logic) ---
func (r *WorkoutRepository) UpdateWorkout(updated *parser.Workout) error {
return errors.New("UpdateWorkout not implemented")
}
func (r *WorkoutRepository) UpdateSetGroup(updated *parser.SetGroup) error {
return errors.New("UpdateSetGroup not implemented")
}
func (r *WorkoutRepository) UpdateSet(updated *parser.Set) error {
return errors.New("UpdateSet not implemented")
}
// --- DELETE METHOD ---
func (r *WorkoutRepository) DeleteWorkout(id string) error {
db := r.db
return db.Update(func(tx *bolt.Tx) error {
bW := tx.Bucket(bucketWorkouts)
if bW == nil {
return errors.New("workouts bucket does not exist")
}
var w Workout
if err := getStruct(bW, id, &w); err != nil {
return err
}
bSG := tx.Bucket(bucketSetGroups)
bS := tx.Bucket(bucketSets)
for _, sgid := range w.SetGroupIDs {
var sg SetGroup
if bSG != nil {
if err := getStruct(bSG, sgid, &sg); err != nil {
continue
}
for _, setid := range sg.PlannedSetIDs {
_ = deleteStruct(bS, setid)
}
for _, setid := range sg.ActualSetIDs {
_ = deleteStruct(bS, setid)
}
_ = deleteStruct(bSG, sgid)
}
}
return deleteStruct(bW, id)
})
}