1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
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
}
}
|