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 } }