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 NewWorkoutRepository(db *bolt.DB) (*WorkoutRepository, error) { 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, err } return &WorkoutRepository{db: db}, nil } // --- 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) }) }