mirror of
https://github.com/muety/wakapi.git
synced 2025-12-05 22:20:24 -08:00
refactor: language mapping augmentation of durations
This commit is contained in:
File diff suppressed because it is too large
Load Diff
2
main.go
2
main.go
@@ -182,7 +182,7 @@ func main() {
|
||||
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
|
||||
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
|
||||
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
|
||||
durationService = services.NewDurationService(durationRepository, heartbeatService, userService)
|
||||
durationService = services.NewDurationService(durationRepository, heartbeatService, userService, languageMappingService)
|
||||
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, durationService, aliasService, projectLabelService)
|
||||
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService, durationService)
|
||||
reportService = services.NewReportService(summaryService, userService, mailService)
|
||||
|
||||
35
mocks/language_mapping_service.go
Normal file
35
mocks/language_mapping_service.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package mocks
|
||||
|
||||
import (
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type LanguageMappingServiceMock struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (l *LanguageMappingServiceMock) GetById(u uint) (*models.LanguageMapping, error) {
|
||||
args := l.Called(u)
|
||||
return args.Get(0).(*models.LanguageMapping), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LanguageMappingServiceMock) GetByUser(s string) ([]*models.LanguageMapping, error) {
|
||||
args := l.Called(s)
|
||||
return args.Get(0).([]*models.LanguageMapping), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LanguageMappingServiceMock) ResolveByUser(s string) (map[string]string, error) {
|
||||
args := l.Called(s)
|
||||
return args.Get(0).(map[string]string), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LanguageMappingServiceMock) Create(m *models.LanguageMapping) (*models.LanguageMapping, error) {
|
||||
args := l.Called(m)
|
||||
return args.Get(0).(*models.LanguageMapping), args.Error(1)
|
||||
}
|
||||
|
||||
func (l *LanguageMappingServiceMock) Delete(m *models.LanguageMapping) error {
|
||||
args := l.Called(m)
|
||||
return args.Error(0)
|
||||
}
|
||||
@@ -4,7 +4,9 @@ import (
|
||||
"fmt"
|
||||
"github.com/cespare/xxhash/v2"
|
||||
"github.com/gohugoio/hashstructure"
|
||||
"github.com/muety/wakapi/models/lib"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
@@ -92,6 +94,19 @@ func (d *Duration) Hashed() *Duration {
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Duration) Augmented(languageMappings map[string]string) *Duration {
|
||||
for ext, targetLang := range languageMappings {
|
||||
langs, ok := lib.LanguagesByExtension["."+ext]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if lang := langs[0]; strings.ToLower(d.Language) == strings.ToLower(lang) {
|
||||
d.Language = targetLang
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (d *Duration) GetKey(t uint8) (key string) {
|
||||
switch t {
|
||||
case SummaryProject:
|
||||
|
||||
@@ -44,3 +44,10 @@ func (d *Durations) Last() *Duration {
|
||||
}
|
||||
return (*d)[d.Len()-1]
|
||||
}
|
||||
|
||||
func (d Durations) Augmented(languageMappings map[string]string) Durations {
|
||||
for _, item := range d {
|
||||
item.Augmented(languageMappings)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
2097
models/lib/extensions.go
Normal file
2097
models/lib/extensions.go
Normal file
File diff suppressed because it is too large
Load Diff
@@ -20,24 +20,26 @@ const heartbeatPadding = 0 * time.Second
|
||||
const generateDurationsInterval = 12 * time.Hour
|
||||
|
||||
type DurationService struct {
|
||||
config *config.Config
|
||||
eventBus *hub.Hub
|
||||
durationRepository repositories.IDurationRepository
|
||||
heartbeatService IHeartbeatService
|
||||
userService IUserService
|
||||
lastUserJob map[string]time.Time
|
||||
queue *artifex.Dispatcher
|
||||
config *config.Config
|
||||
eventBus *hub.Hub
|
||||
durationRepository repositories.IDurationRepository
|
||||
heartbeatService IHeartbeatService
|
||||
userService IUserService
|
||||
LanguageMappingService ILanguageMappingService
|
||||
lastUserJob map[string]time.Time
|
||||
queue *artifex.Dispatcher
|
||||
}
|
||||
|
||||
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService) *DurationService {
|
||||
func NewDurationService(durationRepository repositories.IDurationRepository, heartbeatService IHeartbeatService, userService IUserService, languageMappingService ILanguageMappingService) *DurationService {
|
||||
srv := &DurationService{
|
||||
config: config.Get(),
|
||||
eventBus: config.EventBus(),
|
||||
heartbeatService: heartbeatService,
|
||||
userService: userService,
|
||||
durationRepository: durationRepository,
|
||||
lastUserJob: make(map[string]time.Time),
|
||||
queue: config.GetQueue(config.QueueProcessing),
|
||||
config: config.Get(),
|
||||
eventBus: config.EventBus(),
|
||||
heartbeatService: heartbeatService,
|
||||
userService: userService,
|
||||
LanguageMappingService: languageMappingService,
|
||||
durationRepository: durationRepository,
|
||||
lastUserJob: make(map[string]time.Time),
|
||||
queue: config.GetQueue(config.QueueProcessing),
|
||||
}
|
||||
|
||||
// TODO: refactor to updating durations on-the-fly as heartbeats flow in, instead of batch-wise
|
||||
@@ -56,23 +58,6 @@ func NewDurationService(durationRepository repositories.IDurationRepository, hea
|
||||
}
|
||||
}(&sub1)
|
||||
|
||||
sub2 := srv.eventBus.Subscribe(0, config.EventLanguageMappingsChanged)
|
||||
go func(sub *hub.Subscription) {
|
||||
for m := range sub.Receiver {
|
||||
userId := m.Fields[config.FieldUserId].(string)
|
||||
user, err := srv.userService.GetUserById(userId)
|
||||
if err != nil {
|
||||
config.Log().Error("user not found for regenerating durations after language mapping change", "user", userId)
|
||||
continue
|
||||
}
|
||||
|
||||
slog.Info("regenerating durations because language mappings were updated", "user", userId)
|
||||
srv.queue.Dispatch(func() {
|
||||
srv.Regenerate(user, true)
|
||||
})
|
||||
}
|
||||
}(&sub2)
|
||||
|
||||
return srv
|
||||
}
|
||||
|
||||
@@ -95,7 +80,8 @@ func (srv *DurationService) Get(from, to time.Time, user *models.User, filters *
|
||||
// get cached
|
||||
cached, err := srv.getCached(from, to, user, filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
config.Log().Error("failed to get cached durations", "user", user.ID, "from", from, "to", to, "error", err)
|
||||
cached = models.Durations{}
|
||||
}
|
||||
|
||||
// fill missing
|
||||
@@ -167,11 +153,15 @@ func (srv *DurationService) RegenerateAll() {
|
||||
}
|
||||
|
||||
func (srv *DurationService) getCached(from, to time.Time, user *models.User, filters *models.Filters) (models.Durations, error) {
|
||||
languageMappings, err := srv.LanguageMappingService.ResolveByUser(user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
durations, err := srv.durationRepository.GetAllWithinByFilters(from, to, user, srv.filtersToColumnMap(filters))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return models.Durations(durations).Sorted(), nil
|
||||
return models.Durations(durations).Augmented(languageMappings).Sorted(), nil
|
||||
}
|
||||
|
||||
func (srv *DurationService) getLive(from, to time.Time, user *models.User, interval time.Duration) (models.Durations, error) {
|
||||
|
||||
@@ -38,13 +38,14 @@ const (
|
||||
|
||||
type DurationServiceTestSuite struct {
|
||||
suite.Suite
|
||||
TestUser *models.User
|
||||
TestStartTime time.Time
|
||||
TestHeartbeats []*models.Heartbeat
|
||||
TestLabels []*models.ProjectLabel
|
||||
DurationRepository *mocks.DurationRepositoryMock
|
||||
HeartbeatService *mocks.HeartbeatServiceMock
|
||||
UserService *mocks.UserServiceMock
|
||||
TestUser *models.User
|
||||
TestStartTime time.Time
|
||||
TestHeartbeats []*models.Heartbeat
|
||||
TestLabels []*models.ProjectLabel
|
||||
DurationRepository *mocks.DurationRepositoryMock
|
||||
HeartbeatService *mocks.HeartbeatServiceMock
|
||||
UserService *mocks.UserServiceMock
|
||||
LanguageMappingService *mocks.LanguageMappingServiceMock
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) SetupSuite() {
|
||||
@@ -131,6 +132,9 @@ func (suite *DurationServiceTestSuite) BeforeTest(suiteName, testName string) {
|
||||
suite.DurationRepository = new(mocks.DurationRepositoryMock)
|
||||
suite.HeartbeatService = new(mocks.HeartbeatServiceMock)
|
||||
suite.UserService = new(mocks.UserServiceMock)
|
||||
suite.LanguageMappingService = new(mocks.LanguageMappingServiceMock)
|
||||
|
||||
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(make(map[string]string), nil)
|
||||
}
|
||||
|
||||
func TestDurationServiceTestSuite(t *testing.T) {
|
||||
@@ -139,7 +143,7 @@ func TestDurationServiceTestSuite(t *testing.T) {
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
||||
// https://anchr.io/i/F0HEK.jpg
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@@ -188,7 +192,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get() {
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@@ -211,7 +215,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Filtered() {
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@@ -267,7 +271,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomTimeout() {
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@@ -309,7 +313,7 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_Cached() {
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval() {
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService)
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
@@ -329,6 +333,34 @@ func (suite *DurationServiceTestSuite) TestDurationService_Get_CustomInterval()
|
||||
assert.Empty(suite.T(), suite.DurationRepository.Calls)
|
||||
}
|
||||
|
||||
func (suite *DurationServiceTestSuite) TestDurationService_Get_WithLanguageMapping() {
|
||||
suite.LanguageMappingService.ExpectedCalls[0].Unset()
|
||||
suite.LanguageMappingService.On("ResolveByUser", suite.TestUser.ID).Return(map[string]string{"go": "Golang"}, nil)
|
||||
|
||||
sut := NewDurationService(suite.DurationRepository, suite.HeartbeatService, suite.UserService, suite.LanguageMappingService)
|
||||
|
||||
var (
|
||||
from time.Time
|
||||
to time.Time
|
||||
durations models.Durations
|
||||
err error
|
||||
)
|
||||
|
||||
testDurations := []*models.Duration{
|
||||
models.NewDurationFromHeartbeat(suite.TestHeartbeats[0]),
|
||||
}
|
||||
|
||||
from, to = suite.TestStartTime.Add(-1*time.Hour), suite.TestStartTime.Add(1*time.Hour)
|
||||
suite.DurationRepository.On("GetAllWithinByFilters", from, to, suite.TestUser, mock.Anything).Return(testDurations, nil)
|
||||
suite.HeartbeatService.On("StreamAllWithin", mock.Anything, mock.Anything, suite.TestUser).Return(streamSlice([]*models.Heartbeat{}), nil)
|
||||
|
||||
durations, err = sut.Get(from, to, suite.TestUser, nil, nil, false)
|
||||
|
||||
assert.Nil(suite.T(), err)
|
||||
assert.Len(suite.T(), durations, 1)
|
||||
assert.Equal(suite.T(), "Golang", durations.First().Language)
|
||||
}
|
||||
|
||||
func filterHeartbeats(from, to time.Time, heartbeats []*models.Heartbeat) []*models.Heartbeat {
|
||||
filtered := make([]*models.Heartbeat, 0, len(heartbeats))
|
||||
for _, h := range heartbeats {
|
||||
|
||||
Reference in New Issue
Block a user