refactor to use app package instead of service

refactor app to be composable
This commit is contained in:
Robin Olsen
2026-02-28 23:30:31 +01:00
parent ba4d3df669
commit aabce0b0b2
56 changed files with 385 additions and 279 deletions

66
internal/app/app.go Normal file
View File

@@ -0,0 +1,66 @@
package app
import (
"context"
"time"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/internal/storage"
"github.com/steveyegge/beads"
)
type App = models.App
type Config = models.Config
func New(ctx context.Context, config Config, opts ...Option) (*App, func(), error) {
b := defaultBuilder(ctx, config)
for _, opt := range opts {
if err := opt(b); err != nil {
return nil, nil, err
}
}
if !config.AutoInit {
if err := b.initializer.Init(config.BeadsDBPath); err != nil {
return nil, nil, err
}
}
if b.issueService == nil {
sqliteStore, err := beads.NewSQLiteStorage(b.ctx, config.BeadsDBPath)
if err != nil {
return nil, nil, err
}
b.lifecycle.Add(func() { sqliteStore.Close() })
b.issueService, err = storage.NewBeadsIssueStorage(b.ctx, sqliteStore, config.IssuePrefix)
if err != nil {
return nil, nil, err
}
}
if b.statsService == nil {
statStore := storage.NewJsonStorage(config.StatisticsStoragePath, &models.Statistics{
ID: 0,
StartTime: time.Now(),
})
jsonStatService, err := NewStatisticsService(statStore)
if err != nil {
return nil, nil, err
}
b.statsService = jsonStatService
}
app := &App{
Config: config,
Logger: b.logger,
Issues: b.issueService,
Stats: b.statsService,
}
return app, b.lifecycle.Close, nil
}

View File

@@ -0,0 +1,41 @@
package app
import (
"fmt"
"os"
"github.com/charmbracelet/huh"
)
type Initializer interface {
Init(path string) error
}
type InteractiveInitializer struct{}
func (i InteractiveInitializer) Init(path string) error {
_, err := os.Stat(path)
if !os.IsNotExist(err) {
return nil
}
var initialize bool
err = huh.NewForm(
huh.NewGroup(
huh.NewConfirm().
Title("PM is not initialized in this directory!").
Description("Do you want to initialize it here?").
Value(&initialize),
)).WithTheme(huh.ThemeBase16()).WithAccessible(true).Run()
if err != nil {
return err
}
if !initialize {
return fmt.Errorf("project not initialized")
}
return nil
}

21
internal/app/lifecycle.go Normal file
View File

@@ -0,0 +1,21 @@
package app
type Lifecycle struct {
cleanups []func()
}
func NewLifecycle() *Lifecycle {
return &Lifecycle{
cleanups: make([]func(), 0),
}
}
func (l *Lifecycle) Add(fn func()) {
l.cleanups = append(l.cleanups, fn)
}
func (l *Lifecycle) Close() {
for i := len(l.cleanups) - 1; i >= 0; i-- {
l.cleanups[i]()
}
}

68
internal/app/options.go Normal file
View File

@@ -0,0 +1,68 @@
package app
import (
"context"
"log/slog"
"os"
"github.com/LazyBachelor/LazyPM/internal/models"
)
type Option func(*AppBuilder) error
type AppBuilder struct {
config Config
ctx context.Context
logger *slog.Logger
lifecycle *Lifecycle
initializer Initializer
issueService models.IssueService
statsService models.StatsService
}
func defaultBuilder(ctx context.Context, config Config) *AppBuilder {
return &AppBuilder{
ctx: ctx,
config: config,
lifecycle: NewLifecycle(),
initializer: &InteractiveInitializer{},
logger: slog.New(slog.NewJSONHandler(os.Stdout, nil)),
}
}
func WithLogger(logger *slog.Logger) Option {
return func(b *AppBuilder) error {
b.logger = logger
return nil
}
}
func WithInitializer(initializer Initializer) Option {
return func(b *AppBuilder) error {
b.initializer = initializer
return nil
}
}
func WithLifecycle(l *Lifecycle) Option {
return func(b *AppBuilder) error {
b.lifecycle = l
return nil
}
}
func WithIssueService(svc models.IssueService) Option {
return func(b *AppBuilder) error {
b.issueService = svc
return nil
}
}
func WithStatsService(svc models.StatsService) Option {
return func(b *AppBuilder) error {
b.statsService = svc
return nil
}
}

View File

@@ -0,0 +1,37 @@
package app
import (
"context"
"errors"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/LazyBachelor/LazyPM/internal/storage"
)
type StatisticsService struct {
storage *storage.Storage[models.Statistics]
}
func NewStatisticsService(storage *storage.Storage[models.Statistics]) (*StatisticsService, error) {
if err := storage.Init(); err != nil {
return nil, err
}
return &StatisticsService{
storage: storage,
}, nil
}
func (s *StatisticsService) Load(ctx context.Context) error {
return s.storage.Load()
}
func (s *StatisticsService) Save(ctx context.Context) error {
return s.storage.Save()
}
func (s *StatisticsService) GetStatistics() (models.Statistics, error) {
if s.storage.Data == nil {
return models.Statistics{}, errors.New("statistics data not initialized")
}
return *s.storage.Data, nil
}