Skip to content

Commit

Permalink
feat(tempo): add basic tempo client implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
gabor-boros committed Oct 8, 2021
1 parent 2501bcc commit 202ac41
Show file tree
Hide file tree
Showing 2 changed files with 778 additions and 0 deletions.
172 changes: 172 additions & 0 deletions internal/pkg/client/tempo/tempo.go
@@ -0,0 +1,172 @@
package tempo

import (
"context"
"encoding/json"
"fmt"
"math"
"net/http"
"strconv"
"time"

"github.com/gabor-boros/minutes/internal/pkg/client"
"github.com/gabor-boros/minutes/internal/pkg/worklog"
)

const (
// WorklogCreatePath is the endpoint used to create new worklogs.
WorklogCreatePath string = "/rest/tempo-timesheets/4/worklogs"
// WorklogSearchPath is the endpoint used to search existing worklogs.
WorklogSearchPath string = "/rest/tempo-timesheets/4/worklogs/search"
)

// Issue represents the Jira issue the time logged against.
type Issue struct {
ID int `json:"id"`
Key string `json:"key"`
AccountKey string `json:"accountKey"`
ProjectID int `json:"projectId"`
ProjectKey string `json:"projectKey"`
Summary string `json:"summary"`
}

// FetchEntry represents the entry fetched from Tempo.
// StartDate must be in the given YYYY-MM-DD format, required by Tempo.
type FetchEntry struct {
ID int `json:"id"`
StartDate time.Time `json:"startDate"`
BillableSeconds int `json:"billableSeconds"`
TimeSpentSeconds int `json:"timeSpentSeconds"`
Comment string `json:"comment"`
WorkerKey string `json:"workerKey"`
Issue Issue `json:"issue"`
}

// UploadEntry represents the payload to create a new worklog in Tempo.
// Started must be in the given YYYY-MM-DD format, required by Tempo.
type UploadEntry struct {
Comment string `json:"comment,omitempty"`
IncludeNonWorkingDays bool `json:"includeNonWorkingDays,omitempty"`
OriginTaskID string `json:"originTaskId,omitempty"`
Started string `json:"started,omitempty"`
BillableSeconds int `json:"billableSeconds,omitempty"`
TimeSpentSeconds int `json:"timeSpentSeconds,omitempty"`
Worker string `json:"worker,omitempty"`
}

// SearchParams represents the parameters used to filter Tempo search results.
// From and To must be in the given YYYY-MM-DD format, required by Tempo.
type SearchParams struct {
From string `json:"from"`
To string `json:"to"`
Worker string `json:"worker"`
}

// ClientOpts is the client specific options, extending client.BaseClientOpts.
type ClientOpts struct {
client.BaseClientOpts
}

type tempoClient struct {
opts *ClientOpts
}

func (c *tempoClient) FetchEntries(ctx context.Context, opts *client.FetchOpts) (*[]worklog.Entry, error) {
searchParams := &SearchParams{
From: opts.Start.Local().Format("2006-01-02"),
To: opts.End.Local().Format("2006-01-02"),
Worker: opts.User,
}

resp, err := client.SendRequest(ctx, http.MethodPost, WorklogSearchPath, searchParams, &c.opts.HTTPClientOptions)
if err != nil {
return nil, fmt.Errorf("%v: %v", client.ErrFetchEntries, err)
}

var entries []FetchEntry
if err = json.NewDecoder(resp.Body).Decode(&entries); err != nil {
return nil, fmt.Errorf("%v: %v", client.ErrFetchEntries, err)
}

var items []worklog.Entry
for _, entry := range entries {
items = append(items, worklog.Entry{
Client: worklog.IDNameField{
ID: entry.Issue.AccountKey,
Name: entry.Issue.AccountKey,
},
Project: worklog.IDNameField{
ID: strconv.Itoa(entry.Issue.ProjectID),
Name: entry.Issue.ProjectKey,
},
Task: worklog.IDNameField{
ID: strconv.Itoa(entry.Issue.ID),
Name: entry.Issue.Key,
},
Summary: entry.Issue.Summary,
Notes: entry.Comment,
Start: entry.StartDate,
BillableDuration: time.Second * time.Duration(entry.BillableSeconds),
UnbillableDuration: time.Second * time.Duration(entry.TimeSpentSeconds-entry.BillableSeconds),
})
}

return &items, nil
}

func (c *tempoClient) uploadEntry(ctx context.Context, item worklog.Entry, opts *client.UploadOpts, errChan chan error) {
billableDuration := item.BillableDuration
unbillableDuration := item.UnbillableDuration
totalTimeSpent := billableDuration + unbillableDuration

if opts.TreatDurationAsBilled {
billableDuration = item.UnbillableDuration + item.BillableDuration
unbillableDuration = 0
}

if opts.RoundToClosestMinute {
billableDuration = time.Second * time.Duration(math.Round(billableDuration.Minutes())*60)
unbillableDuration = time.Second * time.Duration(math.Round(unbillableDuration.Minutes())*60)
totalTimeSpent = billableDuration + unbillableDuration
}

entry := &UploadEntry{
Comment: item.Summary,
IncludeNonWorkingDays: true,
OriginTaskID: item.Task.Name,
Started: item.Start.Local().Format("2006-01-02"),
BillableSeconds: int(billableDuration.Seconds()),
TimeSpentSeconds: int(totalTimeSpent.Seconds()),
Worker: opts.User,
}

if _, err := client.SendRequest(ctx, http.MethodPost, WorklogCreatePath, entry, &c.opts.HTTPClientOptions); err != nil {
errChan <- err
return
}

errChan <- nil
}

func (c *tempoClient) UploadEntries(ctx context.Context, items []worklog.Entry, opts *client.UploadOpts) error {
errChan := make(chan error)

for _, item := range items {
go c.uploadEntry(ctx, item, opts, errChan)
}

for i := 0; i < len(items); i++ {
if err := <-errChan; err != nil {
return fmt.Errorf("%v: %v", client.ErrUploadEntries, err)
}
}

return nil
}

// NewClient returns a new Tempo client.
func NewClient(opts *ClientOpts) client.FetchUploader {
return &tempoClient{
opts: opts,
}
}

0 comments on commit 202ac41

Please sign in to comment.