feat: add Config + Service wiring for cache and background sync
This commit is contained in:
parent
74f3c03013
commit
7d799c74f1
4
main.go
4
main.go
|
|
@ -1 +1,5 @@
|
|||
// Package dyzurbot is a Telegram duty-rota bot. It serves the rota from a
|
||||
// local cache (see Store) that is periodically actualized from Google Sheets
|
||||
// (see Syncer/Service). Telegram handlers and reminders are built on top of
|
||||
// Service and read via Store.Snapshot.
|
||||
package dyzurbot
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
package dyzurbot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Config holds the cache/sync settings, sourced from the environment/.env.
|
||||
type Config struct {
|
||||
SpreadsheetID string
|
||||
SheetRange string
|
||||
CachePath string
|
||||
SyncInterval time.Duration
|
||||
}
|
||||
|
||||
// LoadConfig reads configuration from the environment (and .env if present),
|
||||
// applying defaults for everything except the required spreadsheet ID.
|
||||
func LoadConfig() (Config, error) {
|
||||
_ = godotenv.Load()
|
||||
|
||||
cfg := Config{
|
||||
SpreadsheetID: os.Getenv("SPREADSHEET_ID"),
|
||||
SheetRange: getenvDefault("SHEET_RANGE", "Dyżury!A1:F100"),
|
||||
CachePath: getenvDefault("ROTA_CACHE_PATH", "rota.json"),
|
||||
}
|
||||
if cfg.SpreadsheetID == "" {
|
||||
return Config{}, fmt.Errorf("missing SPREADSHEET_ID in environment/.env")
|
||||
}
|
||||
|
||||
raw := getenvDefault("SYNC_INTERVAL", "30m")
|
||||
interval, err := time.ParseDuration(raw)
|
||||
if err != nil {
|
||||
return Config{}, fmt.Errorf("invalid SYNC_INTERVAL %q: %w", raw, err)
|
||||
}
|
||||
cfg.SyncInterval = interval
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func getenvDefault(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// Service ties the Store and Syncer together for the bot to consume.
|
||||
type Service struct {
|
||||
Store *Store
|
||||
Syncer *Syncer
|
||||
interval time.Duration
|
||||
}
|
||||
|
||||
// NewService builds the cache + syncer from config.
|
||||
func NewService(cfg Config) *Service {
|
||||
store := NewStore(cfg.CachePath)
|
||||
syncer := NewSyncer(store, SheetFetcher(cfg.SpreadsheetID, cfg.SheetRange))
|
||||
return &Service{Store: store, Syncer: syncer, interval: cfg.SyncInterval}
|
||||
}
|
||||
|
||||
// Start loads the cached rota, runs one best-effort sync (a failure leaves the
|
||||
// cached/empty data in place — offline-tolerant), then polls in the background
|
||||
// until ctx is canceled.
|
||||
func (s *Service) Start(ctx context.Context) error {
|
||||
if err := s.Store.Load(); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.Syncer.Sync(ctx)
|
||||
go s.Syncer.Run(ctx, s.interval)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package dyzurbot
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestLoadConfigDefaults(t *testing.T) {
|
||||
t.Setenv("SPREADSHEET_ID", "sheet-123")
|
||||
t.Setenv("SHEET_RANGE", "")
|
||||
t.Setenv("ROTA_CACHE_PATH", "")
|
||||
t.Setenv("SYNC_INTERVAL", "")
|
||||
|
||||
cfg, err := LoadConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig: %v", err)
|
||||
}
|
||||
if cfg.SpreadsheetID != "sheet-123" {
|
||||
t.Errorf("SpreadsheetID: got %q", cfg.SpreadsheetID)
|
||||
}
|
||||
if cfg.SheetRange != "Dyżury!A1:F100" {
|
||||
t.Errorf("SheetRange default: got %q", cfg.SheetRange)
|
||||
}
|
||||
if cfg.CachePath != "rota.json" {
|
||||
t.Errorf("CachePath default: got %q", cfg.CachePath)
|
||||
}
|
||||
if cfg.SyncInterval != 30*time.Minute {
|
||||
t.Errorf("SyncInterval default: got %v", cfg.SyncInterval)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRequiresSpreadsheetID(t *testing.T) {
|
||||
t.Setenv("SPREADSHEET_ID", "")
|
||||
if _, err := LoadConfig(); err == nil {
|
||||
t.Fatal("expected error when SPREADSHEET_ID is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfigRejectsBadInterval(t *testing.T) {
|
||||
t.Setenv("SPREADSHEET_ID", "sheet-123")
|
||||
t.Setenv("SYNC_INTERVAL", "not-a-duration")
|
||||
if _, err := LoadConfig(); err == nil {
|
||||
t.Fatal("expected error for unparseable SYNC_INTERVAL")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue