aboutsummaryrefslogtreecommitdiff
path: root/internal/tracker
diff options
context:
space:
mode:
authorAlexander Kiryukhin <a.kiryukhin@mail.ru>2021-12-05 17:46:53 +0300
committerAlexander Kiryukhin <a.kiryukhin@mail.ru>2021-12-05 17:46:53 +0300
commitbcdbe68ecde049ef62343584bcc26840322c4864 (patch)
tree4a02b4da5db29ab3f3526ff475db859293a97646 /internal/tracker
Initial commit
Diffstat (limited to 'internal/tracker')
-rw-r--r--internal/tracker/tracker.go132
-rw-r--r--internal/tracker/tracker_test.go68
-rw-r--r--internal/tracker/types.go31
3 files changed, 231 insertions, 0 deletions
diff --git a/internal/tracker/tracker.go b/internal/tracker/tracker.go
new file mode 100644
index 0000000..6d9f058
--- /dev/null
+++ b/internal/tracker/tracker.go
@@ -0,0 +1,132 @@
+package tracker
+
+import (
+ "encoding/json"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/spf13/afero"
+)
+
+var (
+ ErrEntryAlreadyExists = fmt.Errorf("entry with this title already exists")
+ ErrActivityNotFound = fmt.Errorf("there is no activity with given id")
+)
+
+type Tracker struct {
+ fs afero.Fs
+ document *Document
+}
+
+const FileName = "gotrack.json"
+
+func New(fs afero.Fs) (*Tracker, error) {
+ t := &Tracker{fs: fs}
+ if err := t.load(); err != nil {
+ return nil, err
+ }
+ return t, nil
+}
+
+func (t *Tracker) Add(title string, tags []string, contexts []string) (int, error) {
+ for _, activity := range t.document.Activities {
+ if strings.ToLower(strings.Trim(activity.Title, " ")) == strings.ToLower(strings.Trim(title, " ")) {
+ return 0, ErrEntryAlreadyExists
+ }
+ }
+ activity := Activity{
+ ID: t.document.LastKey + 1,
+ Title: title,
+ Tags: tags,
+ Context: contexts,
+ Spans: []*Span{},
+ }
+
+ t.document = &Document{
+ LastKey: activity.ID,
+ Activities: append(t.document.Activities, &activity),
+ }
+
+ return t.document.LastKey, t.save()
+}
+
+func (t *Tracker) Start(id int, comment string) error {
+ if err := t.Stop(id); err != nil {
+ return err
+ }
+ span := &Span{
+ Start: time.Now(),
+ Comment: comment,
+ }
+ activity := t.Activity(id)
+ activity.Spans = append(activity.Spans, span)
+ return t.save()
+}
+
+func (t *Tracker) Stop(id int) error {
+ activity := t.Activity(id)
+ if activity == nil {
+ return ErrActivityNotFound
+ }
+ if span := activity.Started(); span != nil {
+ t := time.Now()
+ span.Stop = &t
+ }
+ return t.save()
+}
+
+func (t *Tracker) List(all bool) []*Activity {
+ if all {
+ return t.document.Activities
+ }
+ return filterActivities(t.document.Activities, func(a *Activity) bool {
+ return a.Started() != nil
+ })
+}
+
+func (t *Tracker) Activity(id int) *Activity {
+ for _, activity := range t.document.Activities {
+ if activity.ID == id {
+ return activity
+ }
+ }
+ return nil
+}
+
+func (t *Tracker) load() (err error) {
+ t.document = new(Document)
+ f, err := t.fs.Open(FileName)
+ defer func() {
+ err = f.Close()
+ }()
+ if err != nil {
+ f, err = t.fs.Create(FileName)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+ return json.NewDecoder(f).Decode(t.document)
+}
+
+func (t *Tracker) save() (err error) {
+ f, err := t.fs.Create(FileName)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ err = f.Close()
+ }()
+ return json.NewEncoder(f).Encode(t.document)
+}
+
+func filterActivities(list []*Activity, filter func(activity *Activity) bool) []*Activity {
+ var filtered []*Activity
+ for _, activity := range list {
+ if filter(activity) {
+ filtered = append(filtered, activity)
+ }
+ }
+ return filtered
+}
diff --git a/internal/tracker/tracker_test.go b/internal/tracker/tracker_test.go
new file mode 100644
index 0000000..c116174
--- /dev/null
+++ b/internal/tracker/tracker_test.go
@@ -0,0 +1,68 @@
+package tracker
+
+import (
+ "testing"
+ "time"
+
+ "github.com/spf13/afero"
+)
+
+func TestTracker(t *testing.T) {
+ fs := afero.NewMemMapFs()
+ tracker, err := New(fs)
+ if err != nil {
+ t.Errorf("Must no err, got %v", err)
+ }
+ tid1, err := tracker.Add("activity 1", []string{}, []string{})
+ if err != nil {
+ t.Errorf("Must no err, got %v", err)
+ }
+ if tid1 != 1 {
+ t.Errorf("Expected task id = 1, got %d", tid1)
+ }
+ tid2, err := tracker.Add("activity 2", []string{"tag1", "tag2"}, []string{"context1"})
+ if err != nil {
+ t.Errorf("Must no err, got %v", err)
+ }
+ if tid2 != 2 {
+ t.Errorf("Expected task id = 2, got %d", tid2)
+ }
+ if err = tracker.Start(tid1, "work 1"); err != nil {
+ t.Errorf("Must no err, got %v", err)
+ }
+ list := tracker.List(false)
+ if len(list) != 1 {
+ t.Errorf("List %v expected to be from 1 element", list)
+ }
+ list2 := tracker.List(true)
+ if len(list2) != 2 {
+ t.Errorf("List %v expected to be from 2 elements", list2)
+ }
+ <- time.After(2 * time.Second)
+ if err := tracker.Stop(tid1); err != nil {
+ t.Errorf("Must no err, got %v", err)
+ }
+ list3 := tracker.List(false)
+ if len(list3) != 0 {
+ t.Errorf("List %v expected to be from 0 element", list3)
+ }
+ list4 := tracker.List(true)
+ for _, activity := range list4 {
+ if activity.ID != tid1 {
+ continue
+ }
+ if len(activity.Spans) != 1 {
+ t.Errorf("List %v expected to be from 1 element", activity.Spans)
+ }
+ sp := activity.Spans[0]
+ if sp.Stop == nil {
+ t.Errorf("Span end time must be not empty")
+ }
+ if !sp.Stop.After(sp.Start) {
+ t.Errorf("End span must be after start time")
+ }
+ if int(sp.Stop.Sub(sp.Start).Seconds()) != 2 {
+ t.Errorf("difference between %v and %v must be 2 seconds, got %f", sp.Start, sp.Stop, sp.Stop.Sub(sp.Start).Seconds())
+ }
+ }
+}
diff --git a/internal/tracker/types.go b/internal/tracker/types.go
new file mode 100644
index 0000000..22837fa
--- /dev/null
+++ b/internal/tracker/types.go
@@ -0,0 +1,31 @@
+package tracker
+
+import "time"
+
+type Activity struct {
+ ID int `json:"id"`
+ Title string `json:"title"`
+ Tags []string `json:"tags"`
+ Context []string `json:"context"`
+ Spans []*Span `json:"spans"`
+}
+
+func (a *Activity) Started() *Span {
+ for _, span := range a.Spans {
+ if span.Stop == nil {
+ return span
+ }
+ }
+ return nil
+}
+
+type Span struct {
+ Start time.Time `json:"start"`
+ Stop *time.Time `json:"stop,omitempty"`
+ Comment string `json:"comment,omitempty"`
+}
+
+type Document struct {
+ LastKey int `json:"last"`
+ Activities []*Activity `json:"activities"`
+}