From fdf8b6def431561b9f25a08c05a621280a4dcf65 Mon Sep 17 00:00:00 2001 From: Thomas Schenker Date: Thu, 29 Dec 2022 23:13:51 +0100 Subject: [PATCH] feat: introduce time tracking record key, add time tracking manager interface together with extension of local and S3 repository --- interfaces.go | 11 +++++++ local.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++--- local_test.go | 16 +++++++++ s3.go | 42 ++++++++++++++++++++++-- s3_test.go | 19 +++++++++++ types.go | 3 ++ 6 files changed, 173 insertions(+), 7 deletions(-) diff --git a/interfaces.go b/interfaces.go index 4b2eae6..cba87c4 100644 --- a/interfaces.go +++ b/interfaces.go @@ -19,6 +19,17 @@ type TimeTracker interface { ListRecords(string, time.Time, time.Time) ([]TimeTrackingRecord, error) } +// TimeTrackingRecordManager is used to create, update or delete single time tracking records. +type TimeTrackingRecordManager interface { + + // Add creates a new time tracking record with given values. Same time tacking record will be + // returned together with a generated key. + Add(TimeTrackingRecord) (TimeTrackingRecord, error) + + // Delete will remove time tracking record by passed key. + Delete(string) error +} + // ReportCalculator creates a time tracking summary based on captured records. type ReportCalculator interface { diff --git a/local.go b/local.go index a791e8e..1273725 100644 --- a/local.go +++ b/local.go @@ -1,7 +1,10 @@ package timetracker import ( + "errors" "fmt" + "strconv" + "strings" "time" ) @@ -30,7 +33,14 @@ func (repo *LocaLRepository) Captured(deviceId string, recordType RecordType, ti if _, ok := repo.Records[deviceId][date]; !ok { repo.Records[deviceId][date] = []TimeTrackingRecord{} } - repo.Records[deviceId][date] = append(repo.Records[deviceId][date], TimeTrackingRecord{DeviceId: deviceId, Type: recordType, Timestamp: timestamp, Estimated: false}) + record := TimeTrackingRecord{ + DeviceId: deviceId, + Type: recordType, + Timestamp: timestamp, + Estimated: false, + } + record.Key = repo.recordKey(deviceId, record.Timestamp, len(repo.Records[deviceId][date])) + repo.Records[deviceId][date] = append(repo.Records[deviceId][date], record) return nil } @@ -41,13 +51,84 @@ func (repo *LocaLRepository) ListRecords(deviceId string, start time.Time, end t if end.Before(start) { return records, fmt.Errorf("Invalid range: %s - %s", start, end) } - if deviceREcords, ok := repo.Records[deviceId]; ok { + if deviceRecords, ok := repo.Records[deviceId]; ok { for isDayBeforeOrEqual(start, end) { - if recordsForDay, ok := deviceREcords[asDate(start)]; ok { - records = append(records, recordsForDay...) + if recordsForDay, ok := deviceRecords[asDate(start)]; ok { + for idx, record := range recordsForDay { + record.Key = repo.recordKey(deviceId, start, idx) + records = append(records, record) + } } start = nextDay(start) } } return records, nil } + +// Add creates a new time tracking record with given values. Same time tacking record will be +// returned together with a generated key. +func (repo *LocaLRepository) Add(record TimeTrackingRecord) (TimeTrackingRecord, error) { + + deviceId := record.DeviceId + date := asDate(record.Timestamp) + if _, ok := repo.Records[deviceId]; !ok { + repo.Records[deviceId] = make(map[Date][]TimeTrackingRecord) + } + if _, ok := repo.Records[deviceId][date]; !ok { + repo.Records[deviceId][date] = []TimeTrackingRecord{} + } + record.Key = repo.recordKey(deviceId, record.Timestamp, len(repo.Records[deviceId][date])) + repo.Records[deviceId][date] = append(repo.Records[deviceId][date], record) + return record, nil +} + +// Delete will remove given time tracking record. +func (repo *LocaLRepository) Delete(key string) error { + + keyParts := strings.Split(key, "/") + if len(keyParts) != 3 { + return errors.New("Invalid key passed.") + } + + deviceId := keyParts[0] + _, ok := repo.Records[deviceId] + if !ok { + return errors.New("Invalid deviceid: " + deviceId) + } + + dateStr := keyParts[1] + timestamp, err := time.Parse("2006-01-02", dateStr) + if err != nil { + return err + } + + date := asDate(timestamp) + _, ok1 := repo.Records[deviceId][date] + if !ok1 { + return errors.New("Invalid date: " + dateStr) + } + + idx, err := strconv.Atoi(keyParts[2]) + if err != nil { + return err + } + + if len(repo.Records[deviceId][date])-1 < idx { + return errors.New("Invalid index: " + keyParts[2]) + } + + repo.Records[deviceId][date] = removeRecordAtIndex(repo.Records[deviceId][date], idx) + return nil +} + +// RecordKey compose given part to a record key. +func (repo *LocaLRepository) recordKey(deviceId string, day time.Time, idx int) string { + return fmt.Sprintf("%s/%s/%d", deviceId, asDate(day).String(), idx) +} + +// RemoveRecordAtIndex removes time tracking records from given list at specific index. +func removeRecordAtIndex(records []TimeTrackingRecord, index int) []TimeTrackingRecord { + ret := make([]TimeTrackingRecord, 0) + ret = append(ret, records[:index]...) + return append(ret, records[index+1:]...) +} diff --git a/local_test.go b/local_test.go index 6f3414d..5cfce8b 100644 --- a/local_test.go +++ b/local_test.go @@ -66,6 +66,22 @@ func (suite *LocalRepositoryTestSuite) TestListRecords() { suite.Len(records3, 0) } +func (suite *LocalRepositoryTestSuite) TestRecordCrudActions() { + + repo := NewLocaLRepository() + record := TimeTrackingRecord{ + DeviceId: "Device01", + Type: WORKDAY, + Timestamp: time.Now(), + } + + record1, err := repo.Add(record) + suite.Nil(err) + suite.True(len(record1.Key) > 0) + + suite.Nil(repo.Delete(record1.Key)) +} + func prepareRecords(repo *LocaLRepository, deviceId string) { durations := []time.Duration{ diff --git a/s3.go b/s3.go index 0221261..7180551 100644 --- a/s3.go +++ b/s3.go @@ -63,18 +63,24 @@ func (repo *S3Repository) Capture(deviceId string, recordType RecordType) error // Captured creates a time tracking record for passed point in time. func (repo *S3Repository) Captured(deviceId string, recordType RecordType, timestamp time.Time) error { - timeTrackingRecord := TimeTrackingRecord{DeviceId: deviceId, Type: recordType, Timestamp: timestamp.UTC()} - content, _ := json.Marshal(timeTrackingRecord) + timeTrackingRecord := TimeTrackingRecord{ + DeviceId: deviceId, + Type: recordType, + Timestamp: timestamp.UTC(), + } objectPath := repo.newS3ObjectPath(deviceId, timeTrackingRecord.Timestamp) + timeTrackingRecord.Key = *objectPath + repo.newRecordId() + content, _ := json.Marshal(timeTrackingRecord) uploadInput := &s3manager.UploadInput{ Bucket: repo.bucket, - Key: aws.String(*objectPath + repo.newRecordId()), + Key: aws.String(timeTrackingRecord.Key), Body: bytes.NewReader(content), } _, uploadErr := repo.uploader.Upload(uploadInput) return uploadErr } +// ListRecords returns all records captured for given device id and time range. func (repo *S3Repository) ListRecords(deviceId string, start time.Time, end time.Time) ([]TimeTrackingRecord, error) { records := []TimeTrackingRecord{} @@ -117,11 +123,41 @@ func (repo *S3Repository) ListRecords(deviceId string, start time.Time, end time if decodeErr != nil { return records, decodeErr } + timeTrackingRecord.Key = *key records = append(records, *timeTrackingRecord) } return records, nil } +// Add creates a new time tracking record with given values. Same time tacking record will be +// returned together with a generated key. +func (repo *S3Repository) Add(record TimeTrackingRecord) (TimeTrackingRecord, error) { + + objectPath := repo.newS3ObjectPath(record.DeviceId, record.Timestamp) + record.Key = *objectPath + repo.newRecordId() + + content, _ := json.Marshal(record) + uploadInput := &s3manager.UploadInput{ + Bucket: repo.bucket, + Key: aws.String(record.Key), + Body: bytes.NewReader(content), + } + _, err := repo.uploader.Upload(uploadInput) + return record, err +} + +// Delete will remove given time tracking record. +func (repo *S3Repository) Delete(key string) error { + + deleteObjectInput := &s3.DeleteObjectInput{ + Bucket: repo.bucket, + Key: aws.String(key), + } + + _, err := repo.s3.DeleteObject(deleteObjectInput) + return err +} + // NewS3ObjectPath create a S3 object key including passed device id and date. // Will add a path prefix if it has been defined at creating this repository. func (repo *S3Repository) newS3ObjectPath(deviceId string, t time.Time) *string { diff --git a/s3_test.go b/s3_test.go index 5250af3..bf121b9 100644 --- a/s3_test.go +++ b/s3_test.go @@ -49,6 +49,25 @@ func (suite *S3TestSuite) TestListRecords() { suite.Len(records1, 0) } +func (suite *S3TestSuite) TestRecordCrudActions() { + + suite.skipCI() + + repo := suite.s3RepoForTest() + + record := TimeTrackingRecord{ + DeviceId: "Device01", + Type: WORKDAY, + Timestamp: time.Now(), + } + + record1, err := repo.Add(record) + suite.Nil(err) + suite.True(len(record1.Key) > 0) + + suite.Nil(repo.Delete(record1.Key)) +} + func (suite *S3TestSuite) TestPublishReport() { suite.skipCI() diff --git a/types.go b/types.go index 80a98ed..32191a2 100644 --- a/types.go +++ b/types.go @@ -42,6 +42,9 @@ type MonthlyReport struct { // TimeTrackingReport os a single captured time tracking event. type TimeTrackingRecord struct { + // Key is an unique identifier of a time tracking record. + Key string + // DeviceId is an identifier of a device which captures a time tracking record. DeviceId string