Consolidate CLI report date policy
This commit is contained in:
@@ -69,36 +69,6 @@ func TestResolveComparisonActionBuildsExplicitRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComparisonActionMatchesReportDatePolicies(t *testing.T) {
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\n timezone: America/Chicago\n")
|
||||
runner := comparisonRunner(t, t.TempDir())
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
args []string
|
||||
wantDay string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "daily requires date", args: []string{"daily", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
{name: "today uses current local date", args: []string{"today", "--profile", "one", "--profile", "two", "--config", configPath}, wantDay: "2026-05-29"},
|
||||
{name: "today accepts date", args: []string{"today", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantDay: "2026-05-30"},
|
||||
{name: "tomorrow rejects date", args: []string{"tomorrow", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
{name: "hourly rejects date", args: []string{"hourly", "--date", "2026-05-30", "--profile", "one", "--profile", "two", "--config", configPath}, wantErr: true},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
req, _, err := runner.resolveComparisonAction(test.args)
|
||||
if test.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("resolveComparisonAction() error = nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || req.Date.Format("2006-01-02") != test.wantDay {
|
||||
t.Fatalf("request/error = %#v/%v", req, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveComparisonActionLeavesConfiguredOutputWithoutOverride(t *testing.T) {
|
||||
workingDir := t.TempDir()
|
||||
configPath := comparisonConfigPath(t, "weather_api:\n base_url: https://weather.api.example.com/\noutput:\n directory: configured/../reports\n")
|
||||
|
||||
75
internal/cli/report_date.go
Normal file
75
internal/cli/report_date.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
|
||||
)
|
||||
|
||||
var (
|
||||
errReportDateRequired = errors.New("report date is required")
|
||||
errReportDateNotAccepted = errors.New("report does not accept a date")
|
||||
)
|
||||
|
||||
type reportDatePolicy struct {
|
||||
acceptsDate bool
|
||||
requiresDate bool
|
||||
defaultsToLocalDate bool
|
||||
}
|
||||
|
||||
func reportDatePolicyFor(report app.ReportKind) reportDatePolicy {
|
||||
switch report {
|
||||
case app.ReportDaily:
|
||||
return reportDatePolicy{acceptsDate: true, requiresDate: true}
|
||||
case app.ReportToday:
|
||||
return reportDatePolicy{acceptsDate: true, defaultsToLocalDate: true}
|
||||
default:
|
||||
return reportDatePolicy{}
|
||||
}
|
||||
}
|
||||
|
||||
func addReportDateFlag(fs interface {
|
||||
StringVar(*string, string, string, string)
|
||||
}, report app.ReportKind, value *string) {
|
||||
if reportDatePolicyFor(report).acceptsDate {
|
||||
fs.StringVar(value, "date", "", "report date in YYYY-MM-DD")
|
||||
}
|
||||
}
|
||||
|
||||
func reportDateRequiredError(action string, report app.ReportKind, value string) error {
|
||||
if reportDatePolicyFor(report).requiresDate && value == "" {
|
||||
return fmt.Errorf("%s %s requires --date YYYY-MM-DD", action, report)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveActionReportDate(action string, report app.ReportKind, value string, location *time.Location, now time.Time) (time.Time, error) {
|
||||
date, err := resolveReportDate(report, value, location, now)
|
||||
if errors.Is(err, errReportDateRequired) {
|
||||
return time.Time{}, fmt.Errorf("%s %s requires --date YYYY-MM-DD", action, report)
|
||||
}
|
||||
return date, err
|
||||
}
|
||||
|
||||
func resolveReportDate(report app.ReportKind, value string, location *time.Location, now time.Time) (time.Time, error) {
|
||||
policy := reportDatePolicyFor(report)
|
||||
if !policy.acceptsDate {
|
||||
if value != "" {
|
||||
return time.Time{}, errReportDateNotAccepted
|
||||
}
|
||||
return time.Time{}, nil
|
||||
}
|
||||
if value == "" {
|
||||
if policy.requiresDate {
|
||||
return time.Time{}, errReportDateRequired
|
||||
}
|
||||
if policy.defaultsToLocalDate {
|
||||
return timeutil.LocalDate(now, location), nil
|
||||
}
|
||||
return time.Time{}, nil
|
||||
}
|
||||
return timeutil.ParseLocalDate(value, location)
|
||||
}
|
||||
101
internal/cli/report_date_test.go
Normal file
101
internal/cli/report_date_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/app"
|
||||
)
|
||||
|
||||
func TestReportDatePolicy(t *testing.T) {
|
||||
location, err := time.LoadLocation("America/New_York")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC)
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
report app.ReportKind
|
||||
value string
|
||||
wantDay string
|
||||
wantErr error
|
||||
malformed bool
|
||||
}{
|
||||
{name: "DailyRequiresDate", report: app.ReportDaily, wantErr: errReportDateRequired},
|
||||
{name: "DailyParsesDate", report: app.ReportDaily, value: "2026-05-30", wantDay: "2026-05-30"},
|
||||
{name: "TodayDefaultsInConfiguredTimezone", report: app.ReportToday, wantDay: "2026-05-29"},
|
||||
{name: "TodayParsesDate", report: app.ReportToday, value: "2026-05-30", wantDay: "2026-05-30"},
|
||||
{name: "TodayRejectsMalformedDate", report: app.ReportToday, value: "not-a-date", malformed: true},
|
||||
{name: "TomorrowHasNoDate", report: app.ReportTomorrow},
|
||||
{name: "TomorrowRejectsDate", report: app.ReportTomorrow, value: "2026-05-30", wantErr: errReportDateNotAccepted},
|
||||
{name: "HourlyHasNoDate", report: app.ReportHourly},
|
||||
{name: "HourlyRejectsDate", report: app.ReportHourly, value: "2026-05-30", wantErr: errReportDateNotAccepted},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
date, err := resolveReportDate(tt.report, tt.value, location, now)
|
||||
if tt.malformed {
|
||||
if err == nil {
|
||||
t.Fatal("resolveReportDate() error = nil")
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantErr != nil {
|
||||
if !errors.Is(err, tt.wantErr) {
|
||||
t.Fatalf("resolveReportDate() error = %v, want %v", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("resolveReportDate() error = %v", err)
|
||||
}
|
||||
if tt.wantDay == "" {
|
||||
if !date.IsZero() {
|
||||
t.Fatalf("resolveReportDate() = %v, want zero date", date)
|
||||
}
|
||||
return
|
||||
}
|
||||
if date.Location() != location || date.Format("2006-01-02") != tt.wantDay {
|
||||
t.Fatalf("resolveReportDate() = %v, want %s in %s", date, tt.wantDay, location)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportDateFlagsFollowPolicy(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
report app.ReportKind
|
||||
acceptsDate bool
|
||||
}{
|
||||
{report: app.ReportDaily, acceptsDate: true},
|
||||
{report: app.ReportToday, acceptsDate: true},
|
||||
{report: app.ReportTomorrow},
|
||||
{report: app.ReportHourly},
|
||||
} {
|
||||
t.Run(string(tt.report), func(t *testing.T) {
|
||||
_, generateErr := parseGenerateFlags(tt.report, []string{"--date", "2026-05-30"})
|
||||
_, comparisonErr := parseComparisonFlags(tt.report, []string{"--date", "2026-05-30"})
|
||||
if tt.acceptsDate && (generateErr != nil || comparisonErr != nil) {
|
||||
t.Fatalf("date flag errors = %v/%v, want accepted", generateErr, comparisonErr)
|
||||
}
|
||||
if !tt.acceptsDate && (generateErr == nil || comparisonErr == nil) {
|
||||
t.Fatalf("date flag errors = %v/%v, want rejected", generateErr, comparisonErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveActionReportDateKeepsActionSpecificMissingDateErrors(t *testing.T) {
|
||||
location := time.UTC
|
||||
now := time.Date(2026, 5, 30, 2, 30, 0, 0, time.UTC)
|
||||
for _, action := range []string{"generate", "compare"} {
|
||||
t.Run(action, func(t *testing.T) {
|
||||
_, err := resolveActionReportDate(action, app.ReportDaily, "", location, now)
|
||||
want := action + " daily requires --date YYYY-MM-DD"
|
||||
if err == nil || err.Error() != want {
|
||||
t.Fatalf("error = %v, want %q", err, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -201,8 +201,8 @@ func (r Runner) resolveGenerateAction(args []string) (app.GenerateRequest, commo
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
if reportKind == app.ReportDaily && opts.Date == "" {
|
||||
return app.GenerateRequest{}, commonOptions{}, fmt.Errorf("generate daily requires --date YYYY-MM-DD")
|
||||
if err := reportDateRequiredError("generate", reportKind, opts.Date); err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
cfg, err := config.Load(config.LoadOptions{
|
||||
Path: opts.ConfigPath,
|
||||
@@ -224,21 +224,9 @@ func (r Runner) resolveGenerateAction(args []string) (app.GenerateRequest, commo
|
||||
Now: now,
|
||||
}
|
||||
|
||||
switch reportKind {
|
||||
case app.ReportDaily:
|
||||
req.Date, err = timeutil.ParseLocalDate(opts.Date, location)
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
case app.ReportToday:
|
||||
if opts.Date == "" {
|
||||
req.Date = timeutil.LocalDate(now, location)
|
||||
} else {
|
||||
req.Date, err = timeutil.ParseLocalDate(opts.Date, location)
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
}
|
||||
req.Date, err = resolveActionReportDate("generate", reportKind, opts.Date, location, now)
|
||||
if err != nil {
|
||||
return app.GenerateRequest{}, commonOptions{}, err
|
||||
}
|
||||
|
||||
workingDir, err := r.workingDir()
|
||||
@@ -307,19 +295,7 @@ func (r Runner) resolveComparisonAction(args []string) (app.ComparisonRequest, c
|
||||
Config: cfg, Report: reportKind, ProfileIDs: append([]string(nil), opts.ProfileIDs...),
|
||||
WorkingDir: workingDir, OutputDir: outputDir, Replace: opts.Replace, LLMDebugDir: opts.LLMDebugDir, Clock: r.Clock,
|
||||
}
|
||||
switch reportKind {
|
||||
case app.ReportDaily:
|
||||
if opts.Date == "" {
|
||||
return app.ComparisonRequest{}, commonOptions{}, fmt.Errorf("compare daily requires --date YYYY-MM-DD")
|
||||
}
|
||||
req.Date, err = timeutil.ParseLocalDate(opts.Date, location)
|
||||
case app.ReportToday:
|
||||
if opts.Date == "" {
|
||||
req.Date = timeutil.LocalDate(r.Clock.Now(), location)
|
||||
} else {
|
||||
req.Date, err = timeutil.ParseLocalDate(opts.Date, location)
|
||||
}
|
||||
}
|
||||
req.Date, err = resolveActionReportDate("compare", reportKind, opts.Date, location, r.Clock.Now())
|
||||
if err != nil {
|
||||
return app.ComparisonRequest{}, commonOptions{}, err
|
||||
}
|
||||
@@ -382,9 +358,7 @@ func parseGenerateFlags(report app.ReportKind, args []string) (generateOptions,
|
||||
opts := generateOptions{}
|
||||
addCommonFlags(fs, &opts.commonOptions, true)
|
||||
fs.BoolVar(&opts.Quiet, "quiet", false, "suppress action summaries and routine batch status output")
|
||||
if report == app.ReportDaily || report == app.ReportToday {
|
||||
fs.StringVar(&opts.Date, "date", "", "report date in YYYY-MM-DD")
|
||||
}
|
||||
addReportDateFlag(fs, report, &opts.Date)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return generateOptions{}, err
|
||||
}
|
||||
@@ -419,9 +393,7 @@ func parseComparisonFlags(report app.ReportKind, args []string) (comparisonOptio
|
||||
fs.BoolVar(&opts.Replace, "replace", false, "replace a recognized comparison bundle")
|
||||
fs.BoolVar(&opts.Quiet, "quiet", false, "suppress action summaries and routine batch status output")
|
||||
fs.Var(&opts.ProfileIDs, "profile", "prompt profile ID")
|
||||
if report == app.ReportDaily || report == app.ReportToday {
|
||||
fs.StringVar(&opts.Date, "date", "", "report date in YYYY-MM-DD")
|
||||
}
|
||||
addReportDateFlag(fs, report, &opts.Date)
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return comparisonOptions{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user