refactor(config): Simplify digest grouping with view parameter #54
No files matched your search
+1
-3
@@ -17,9 +17,7 @@ digest:
|
||||
compress: true
|
||||
mark_as_read: true
|
||||
run_on_startup: false
|
||||
group_by: "category" # choices: category, feed
|
||||
sub_group_by: "day" # choices: date, feed, ai
|
||||
sort_by: "date" # choices: date, title, ai
|
||||
view: "date" # choices: date, category, ai
|
||||
|
||||
ai:
|
||||
api_key: "YOUR_GEMINI_API_KEY"
|
||||
@@ -40,7 +40,7 @@ func (m *mockMinifluxClientService) UpdateEntries(entryIDs []int64, status strin
|
||||
|
||||
type mockDigestService struct{}
|
||||
|
||||
func (m *mockDigestService) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData {
|
||||
func (m *mockDigestService) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,5 @@ import (
|
||||
)
|
||||
|
||||
type DigestService interface {
|
||||
BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData
|
||||
BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData
|
||||
}
|
||||
@@ -45,11 +45,9 @@ type ConfigDigest struct {
|
||||
Email ConfigDigestEmail `koanf:"email"`
|
||||
Schedule string `koanf:"schedule" validate:"gocron"`
|
||||
Host string `koanf:"host"`
|
||||
Compress bool `koanf:"compress"`
|
||||
GroupBy string `koanf:"group_by" validate:"omitempty,oneof=category feed"`
|
||||
SubGroupBy string `koanf:"sub_group_by" validate:"omitempty,oneof=date feed ai"`
|
||||
SortBy string `koanf:"sort_by" validate:"omitempty,oneof=date ai"`
|
||||
MarkAsRead bool `koanf:"mark_as_read"`
|
||||
Compress bool `koanf:"compress"`
|
||||
View string `koanf:"view" validate:"oneof=date category ai"`
|
||||
MarkAsRead bool `koanf:"mark_as_read"`
|
||||
RunOnStartup bool `koanf:"run_on_startup"`
|
||||
}
|
||||
|
||||
@@ -74,8 +72,8 @@ func (c *Config) Validate() error {
|
||||
|
||||
validate.RegisterStructValidation(func(sl validator.StructLevel) {
|
||||
cfg := sl.Current().Interface().(Config)
|
||||
if cfg.Digest.SubGroupBy == "ai" && cfg.AI.ApiKey == "" {
|
||||
sl.ReportError(cfg.AI.ApiKey, "AI.ApiKey", "ApiKey", "required_if", "Digest.SubGroupBy is 'ai'")
|
||||
if cfg.Digest.View == "ai" && cfg.AI.ApiKey == "" {
|
||||
sl.ReportError(cfg.AI.ApiKey, "AI.ApiKey", "ApiKey", "required_if", "Digest.View is 'ai'")
|
||||
}
|
||||
}, Config{})
|
||||
|
||||
@@ -119,9 +117,7 @@ func Load(path string) (*Config, error) {
|
||||
func setDefaultValues(k *koanf.Koanf) error {
|
||||
return k.Load(confmap.Provider(map[string]any{
|
||||
"digest.compress": true,
|
||||
"digest.group_by": "category",
|
||||
"digest.sub_group_by": "feed",
|
||||
"digest.sort_by": "date",
|
||||
"digest.view": "date",
|
||||
"digest.schedule": "@weekly",
|
||||
"digest.mark_as_read": true,
|
||||
"digest.run_on_startup": false,
|
||||
|
||||
@@ -199,8 +199,9 @@ func TestLoad(t *testing.T) {
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "invalid group_by value",
|
||||
name: "invalid view value",
|
||||
config: map[string]any{
|
||||
"miniflux": map[string]any{
|
||||
"host": "miniflux.example.com",
|
||||
@@ -208,13 +209,13 @@ func TestLoad(t *testing.T) {
|
||||
},
|
||||
"digest": map[string]any{
|
||||
"schedule": "@daily",
|
||||
"group_by": "magic",
|
||||
"view": "magic",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "missing ai.api_key when sub_group_by is ai",
|
||||
name: "missing ai.api_key when view is ai",
|
||||
config: map[string]any{
|
||||
"miniflux": map[string]any{
|
||||
"host": "miniflux.example.com",
|
||||
@@ -222,7 +223,7 @@ func TestLoad(t *testing.T) {
|
||||
},
|
||||
"digest": map[string]any{
|
||||
"schedule": "@daily",
|
||||
"sub_group_by": "ai",
|
||||
"view": "ai",
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
|
||||
@@ -56,12 +56,28 @@ type digestServiceImpl struct {
|
||||
llmService services.LLMService
|
||||
}
|
||||
|
||||
func (s *digestServiceImpl) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData {
|
||||
func (s *digestServiceImpl) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData {
|
||||
iconsSlice := make([]*models.FeedIcon, 0, len(icons))
|
||||
for _, icon := range icons {
|
||||
iconsSlice = append(iconsSlice, icon)
|
||||
}
|
||||
|
||||
var groupBy, subGroupBy string
|
||||
switch view {
|
||||
case "date":
|
||||
groupBy = "category"
|
||||
subGroupBy = "date"
|
||||
case "category":
|
||||
groupBy = "category"
|
||||
subGroupBy = "feed"
|
||||
case "ai":
|
||||
groupBy = "category"
|
||||
subGroupBy = "ai"
|
||||
default: // also "category"
|
||||
groupBy = "category"
|
||||
subGroupBy = "feed"
|
||||
}
|
||||
|
||||
primaryGroups := GroupEntries(entries, groupBy)
|
||||
grouper := NewSubGrouper(subGroupBy, s.llmService)
|
||||
allPrimaryGroups, overallDigestSummary := grouper.GroupEntries(primaryGroups)
|
||||
|
||||
@@ -150,8 +150,21 @@ func TestLLMGrouper_GroupEntries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDigestService_BuildDigestData_NonAI(t *testing.T) {
|
||||
mockLLM := &mockLLMService{}
|
||||
func TestDigestService_BuildDigestData(t *testing.T) {
|
||||
mockLLM := &mockLLMService{
|
||||
GenerateContentFunc: func(ctx context.Context, prompt string, schema *genai.Schema) ([]byte, error) {
|
||||
return []byte(`{
|
||||
"overview": "This is a summary of all entries.",
|
||||
"primary_group_summaries": [],
|
||||
"sub_groups": [
|
||||
{
|
||||
"title": "Sub-group 1",
|
||||
"entry_ids": [1, 2]
|
||||
}
|
||||
]
|
||||
}`), nil
|
||||
},
|
||||
}
|
||||
digestService := NewDigestService(mockLLM)
|
||||
|
||||
entries := testutil.CreateMockEntries(0)
|
||||
@@ -160,41 +173,72 @@ func TestDigestService_BuildDigestData_NonAI(t *testing.T) {
|
||||
102: {FeedID: 102, Data: "iconB"},
|
||||
}
|
||||
|
||||
overviewData := digestService.BuildDigestData(entries, icons, "category", "day", "date", "http://miniflux.test")
|
||||
t.Run("view=date", func(t *testing.T) {
|
||||
overviewData := digestService.BuildDigestData(entries, icons, "date", "http://miniflux.test")
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
if len(overviewData.PrimaryGroups) != 3 {
|
||||
t.Errorf("Expected 3 primary groups, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
// Check for date-based sub-grouping
|
||||
catAGroup := findPrimaryGroup(overviewData.PrimaryGroups, "Category A")
|
||||
if catAGroup == nil || len(catAGroup.SubGroups) != 1 {
|
||||
t.Fatalf("Incorrect sub-groups for Category A: %+v", catAGroup)
|
||||
}
|
||||
subGroup := findSubGroup(catAGroup, "Category A - Jan 2, 2024")
|
||||
if subGroup == nil {
|
||||
t.Fatalf("Did not find expected sub-group 'Category A - Jan 2, 2024'")
|
||||
}
|
||||
})
|
||||
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
if len(overviewData.PrimaryGroups) != 3 {
|
||||
t.Errorf("Expected 3 primary groups, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
}
|
||||
t.Run("view=category", func(t *testing.T) {
|
||||
overviewData := digestService.BuildDigestData(entries, icons, "category", "http://miniflux.test")
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
if len(overviewData.PrimaryGroups) != 3 {
|
||||
t.Errorf("Expected 3 primary groups, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
// Check for feed-based sub-grouping
|
||||
catAGroup := findPrimaryGroup(overviewData.PrimaryGroups, "Category A")
|
||||
if catAGroup == nil || len(catAGroup.SubGroups) != 3 {
|
||||
t.Fatalf("Incorrect sub-groups for Category A: %+v", catAGroup)
|
||||
}
|
||||
})
|
||||
|
||||
func TestDigestService_BuildDigestData_LLMFallback(t *testing.T) {
|
||||
entries := testutil.CreateMockEntries(0)
|
||||
icons := map[int64]*models.FeedIcon{
|
||||
100: {FeedID: 100, Data: "icon100"},
|
||||
200: {FeedID: 200, Data: "icon200"},
|
||||
}
|
||||
t.Run("view=ai", func(t *testing.T) {
|
||||
// Note: Using different entries for AI test for simplicity
|
||||
aiEntries := createLLMGrouperMockEntries()
|
||||
overviewData := digestService.BuildDigestData(aiEntries, icons, "ai", "http://miniflux.test")
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
if overviewData.OverviewSummary != "This is a summary of all entries." {
|
||||
t.Errorf("Expected summary, got %q", overviewData.OverviewSummary)
|
||||
}
|
||||
if len(overviewData.PrimaryGroups) != 1 {
|
||||
t.Errorf("Expected 1 primary group, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
})
|
||||
|
||||
mockLLM := &mockLLMService{
|
||||
GenerateContentFunc: func(ctx context.Context, prompt string, schema *genai.Schema) ([]byte, error) {
|
||||
return nil, errors.New("LLM service error during test")
|
||||
},
|
||||
}
|
||||
digestService := NewDigestService(mockLLM)
|
||||
|
||||
overviewData := digestService.BuildDigestData(entries, icons, "category", "ai", "date", "http://miniflux.test")
|
||||
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
|
||||
if overviewData.OverviewSummary != "" {
|
||||
t.Errorf("Expected OverviewSummary to be empty, got %q", overviewData.OverviewSummary)
|
||||
}
|
||||
|
||||
if len(overviewData.PrimaryGroups) != 3 { // Category A, Category B, Category C
|
||||
t.Fatalf("Expected 3 primary groups after fallback, got %d", len(overviewData.PrimaryGroups))
|
||||
}
|
||||
}
|
||||
t.Run("view=ai fallback", func(t *testing.T) {
|
||||
llmErrorService := NewDigestService(&mockLLMService{
|
||||
GenerateContentFunc: func(ctx context.Context, prompt string, schema *genai.Schema) ([]byte, error) {
|
||||
return nil, errors.New("LLM service error")
|
||||
},
|
||||
})
|
||||
overviewData := llmErrorService.BuildDigestData(entries, icons, "ai", "http://miniflux.test")
|
||||
if overviewData == nil {
|
||||
t.Fatal("overviewData is nil")
|
||||
}
|
||||
if overviewData.OverviewSummary != "" {
|
||||
t.Errorf("Expected empty summary on fallback, got %q", overviewData.OverviewSummary)
|
||||
}
|
||||
// On fallback, it should behave like view=category
|
||||
catAGroup := findPrimaryGroup(overviewData.PrimaryGroups, "Category A")
|
||||
if catAGroup == nil || len(catAGroup.SubGroups) != 3 {
|
||||
t.Fatalf("Incorrect sub-groups for Category A on fallback: %+v", catAGroup)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -35,9 +35,7 @@ func ProcessDigest(application *app.App) (*os.File, []*os.File, *models.Overview
|
||||
data := application.DigestService.BuildDigestData(
|
||||
entries,
|
||||
icons,
|
||||
application.Config.Digest.GroupBy,
|
||||
application.Config.Digest.SubGroupBy,
|
||||
application.Config.Digest.SortBy,
|
||||
application.Config.Digest.View,
|
||||
application.Config.Miniflux.Host,
|
||||
)
|
||||
|
||||
|
||||
@@ -30,11 +30,11 @@ func (m *mockMinifluxClientService) UpdateEntries(entryIDs []int64, status strin
|
||||
}
|
||||
|
||||
type mockDigestService struct {
|
||||
buildDigestDataFunc func(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData
|
||||
buildDigestDataFunc func(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData
|
||||
}
|
||||
|
||||
func (m *mockDigestService) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData {
|
||||
return m.buildDigestDataFunc(entries, icons, groupBy, subGroupBy, sortBy, minifluxHost)
|
||||
func (m *mockDigestService) BuildDigestData(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData {
|
||||
return m.buildDigestDataFunc(entries, icons, view, minifluxHost)
|
||||
}
|
||||
|
||||
type mockArchiveService struct {
|
||||
@@ -78,7 +78,7 @@ func TestProcessDigest_Success(t *testing.T) {
|
||||
},
|
||||
}
|
||||
mockDigestService := &mockDigestService{
|
||||
buildDigestDataFunc: func(entries []*models.Entry, icons map[int64]*models.FeedIcon, groupBy string, subGroupBy string, sortBy string, minifluxHost string) *models.OverviewTemplateData {
|
||||
buildDigestDataFunc: func(entries []*models.Entry, icons map[int64]*models.FeedIcon, view string, minifluxHost string) *models.OverviewTemplateData {
|
||||
return expectedOverviewData
|
||||
},
|
||||
}
|
||||
@@ -95,9 +95,7 @@ func TestProcessDigest_Success(t *testing.T) {
|
||||
Digest: config.ConfigDigest{
|
||||
MarkAsRead: true,
|
||||
Compress: true,
|
||||
GroupBy: "category",
|
||||
SubGroupBy: "feed",
|
||||
SortBy: "date",
|
||||
View: "category",
|
||||
},
|
||||
Miniflux: config.ConfigMiniflux{Host: "http://miniflux.test"},
|
||||
}),
|
||||
|
||||
@@ -82,9 +82,7 @@ func generateMockDigest(cfg *config.Config) *models.OverviewTemplateData {
|
||||
data := digestSvc.BuildDigestData(
|
||||
entries,
|
||||
icons,
|
||||
cfg.Digest.GroupBy,
|
||||
cfg.Digest.SubGroupBy,
|
||||
cfg.Digest.SortBy,
|
||||
cfg.Digest.View,
|
||||
cfg.Miniflux.Host,
|
||||
)
|
||||
|
||||
@@ -135,9 +133,7 @@ func generateMinifluxDigest(cfg *config.Config, minifluxClientService services.M
|
||||
return digestSvc.BuildDigestData(
|
||||
entries,
|
||||
icons,
|
||||
cfg.Digest.GroupBy,
|
||||
cfg.Digest.SubGroupBy,
|
||||
cfg.Digest.SortBy,
|
||||
cfg.Digest.View,
|
||||
cfg.Miniflux.Host,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user