diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 21:37:47 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-07-27 21:37:47 +0200 |
| commit | f8c92f253693e269edbc76da9a99e65df7d6f699 (patch) | |
| tree | 0d3024bff169b404d6831422f813431d849913bd /internal/cli/feed.go | |
| parent | bec7c034aca15537900d74a7063d85c0f4deacae (diff) | |
| download | lectio-f8c92f253693e269edbc76da9a99e65df7d6f699.tar.gz lectio-f8c92f253693e269edbc76da9a99e65df7d6f699.zip | |
feat(cli): --format json|ical calendar emitters with range + validation
Diffstat (limited to 'internal/cli/feed.go')
| -rw-r--r-- | internal/cli/feed.go | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/internal/cli/feed.go b/internal/cli/feed.go new file mode 100644 index 0000000..f7e7bab --- /dev/null +++ b/internal/cli/feed.go @@ -0,0 +1,130 @@ +package cli + +import ( + "fmt" + "io" + "strconv" + "time" + + "github.com/lukaszkasprzak/lectio/internal/caldata" + "github.com/lukaszkasprzak/lectio/internal/calendar" + "github.com/lukaszkasprzak/lectio/internal/calfeed" + "github.com/lukaszkasprzak/lectio/internal/config" +) + +// cliMaxSpanDays caps --from/--to at 100 years. The CLI is a local, trusted +// surface (unlike the web endpoints' tighter 1830-day cap), so this exists +// only to catch a fat-fingered year, not to bound resource use. +const cliMaxSpanDays = 36525 + +// minCalendarYear/maxCalendarYear bound --year to the domain the Gregorian +// Computus (Easter algorithm) is valid for; years before the 1582 reform are +// rejected. +const ( + minCalendarYear = 1583 + maxCalendarYear = 9999 +) + +// runFeed handles --format json|ical (+ --from/--to, --year, --form): builds +// the requested day range with the shared calfeed.Build/caldata.Readings +// pipeline (the same one --liturgy uses) and writes the rendered feed to +// stdout. date is the positional DATE (already defaulted to today by +// extractDate); it is used only when neither --from/--to nor --year is +// given. Every validation failure is a fixed stderr message and exit 2; +// nothing is echoed back beyond the offending flag's own value. +func runFeed(cfg config.Config, format, date, fromFlag, toFlag, yearFlag, formFlag string, stdout, stderr io.Writer) int { + if format == "" { + fmt.Fprintln(stderr, "lectio: --from/--to/--year/--form require --format json|ical") + return 2 + } + if format != "json" && format != "ical" { + fmt.Fprintf(stderr, "lectio: invalid --format %q (want json|ical)\n", format) + return 2 + } + + from, to, err := feedRange(date, fromFlag, toFlag, yearFlag) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 2 + } + + sel := cfg.Selection() + if formFlag != "" { + if formFlag != "old" && formFlag != "new" { + fmt.Fprintf(stderr, "lectio: invalid --form %q (want old|new)\n", formFlag) + return 2 + } + sel.Form = formFlag + } + + dir, _ := config.CalendarsDir() + layers, errs := caldata.Stack(sel.Form, dir, cfg.Use) + for _, e := range errs { + fmt.Fprintln(stderr, "lectio: warning:", e) + } + + days := calfeed.Build(from, to, cfg.UILanguage, sel, layers, func(d time.Time, day calendar.LiturgicalDay) []calendar.Reading { + return caldata.Readings(sel, layers, d, day) + }) + + var out []byte + switch format { + case "json": + out, err = calfeed.JSON(sel.Form, days) + if err != nil { + fmt.Fprintln(stderr, "lectio:", err) + return 1 + } + case "ical": + out = calfeed.ICal(sel.Form, days, time.Now()) + } + _, _ = stdout.Write(out) + return 0 +} + +// feedRange resolves --from/--to/--year (mutually exclusive) or, absent +// both, dateStr (the positional DATE) into an inclusive [from, to] range. +// Dates are parsed strictly with time.Parse("2006-01-02", …); --year is +// bounded to [minCalendarYear, maxCalendarYear]; from must not be after to; +// the span is capped at cliMaxSpanDays. Every branch returns a fixed, +// input-scoped error message. +func feedRange(dateStr, fromStr, toStr, yearStr string) (from, to time.Time, err error) { + switch { + case yearStr != "" && (fromStr != "" || toStr != ""): + return time.Time{}, time.Time{}, fmt.Errorf("--year cannot be combined with --from/--to") + case fromStr != "" && toStr == "": + return time.Time{}, time.Time{}, fmt.Errorf("--from requires --to") + case toStr != "" && fromStr == "": + return time.Time{}, time.Time{}, fmt.Errorf("--to requires --from") + case yearStr != "": + year, cerr := strconv.Atoi(yearStr) + if cerr != nil || year < minCalendarYear || year > maxCalendarYear { + return time.Time{}, time.Time{}, fmt.Errorf("invalid --year %q (want an integer %d-%d)", yearStr, minCalendarYear, maxCalendarYear) + } + from = time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) + to = time.Date(year, 12, 31, 0, 0, 0, 0, time.UTC) + return from, to, nil + case fromStr != "" && toStr != "": + from, err = time.Parse("2006-01-02", fromStr) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid --from %q (want YYYY-MM-DD)", fromStr) + } + to, err = time.Parse("2006-01-02", toStr) + if err != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid --to %q (want YYYY-MM-DD)", toStr) + } + if to.Before(from) { + return time.Time{}, time.Time{}, fmt.Errorf("--from %s is after --to %s", fromStr, toStr) + } + if days := int(to.Sub(from).Hours()/24) + 1; days > cliMaxSpanDays { + return time.Time{}, time.Time{}, fmt.Errorf("range too large (%d days; max %d)", days, cliMaxSpanDays) + } + return from, to, nil + default: + d, derr := time.Parse("2006-01-02", dateStr) + if derr != nil { + return time.Time{}, time.Time{}, fmt.Errorf("invalid date %q (want YYYY-MM-DD)", dateStr) + } + return d, d, nil + } +} |
