aboutsummaryrefslogtreecommitdiff
path: root/internal/cli/callayer.go
blob: 3900eb15a29fa79212c3255f7af7186bdb103553 (plain) (blame)
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
131
132
133
134
135
136
package cli

import (
	"fmt"
	"io"
	"os"
	"path/filepath"
	"sort"
	"strings"

	"github.com/lukaszkasprzak/lectio/internal/caldata"
	"github.com/lukaszkasprzak/lectio/internal/calendar"
	"github.com/lukaszkasprzak/lectio/internal/config"
)

// calLayerTemplate is the scaffold written by --cal-new (%s = the layer name).
const calLayerTemplate = `# lectio calendar layer "%[1]s". Add celebrations below, then enable this file
# by adding its name to 'use' in config.ini:  use = %[1]s
# Layers stack over the universal calendar: a [slug] merges over the same slug in
# a lower layer (field by field); a new [slug] adds a celebration.
#
# Fields:
#   date       MM-DD, or easter±N / christmas±N / sunday-after MM-DD
#   rank       solemnity | feast | memorial | optional
#   class      lord | bvm | saint            (precedence tiebreak)
#   colour     white | red | green | violet | rose | black
#   name.<lang>   e.g. name.en, name.pl, name.la
#   reading.<part>   part = first | psalm | second | acclamation | gospel (a citation)
#   suppress = true   remove a celebration inherited from a lower layer

[layer]
id = %[1]s
name = %[1]s
type = diocesan

# Example (edit or delete):
[st-example]
date = 06-15
rank = solemnity
class = saint
colour = white
name.en = Saint Example, Patron
reading.gospel = Jn 10:11-16
`

var validRanks = map[string]bool{"solemnity": true, "feast": true, "memorial": true, "optional": true}

// layerPath resolves <CalendarsDir>/<name>.ini, rejecting a name with path separators.
func layerPath(name string) (string, error) {
	if name == "" || strings.ContainsAny(name, `/\`) {
		return "", fmt.Errorf("invalid layer name %q", name)
	}
	dir, err := config.CalendarsDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(dir, name+".ini"), nil
}

// runCalNew scaffolds a new calendar-layer file and exits. It refuses to
// overwrite an existing file.
func runCalNew(name string, stdout, stderr io.Writer) int {
	path, err := layerPath(name)
	if err != nil {
		fmt.Fprintln(stderr, "lectio:", err)
		return 2
	}
	if _, err := os.Stat(path); err == nil {
		fmt.Fprintf(stderr, "lectio: %s already exists (not overwriting)\n", path)
		return 1
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		fmt.Fprintln(stderr, "lectio:", err)
		return 1
	}
	if err := os.WriteFile(path, []byte(fmt.Sprintf(calLayerTemplate, name)), 0o644); err != nil {
		fmt.Fprintln(stderr, "lectio:", err)
		return 1
	}
	fmt.Fprintf(stdout, "created %s\n", path)
	fmt.Fprintf(stdout, "edit it, then set  use = %s  in config.ini\n", name)
	return 0
}

// runCalCheck validates a calendar-layer file (dates resolve, ranks/colours/
// classes are known) and exits non-zero if it finds problems.
func runCalCheck(name string, stdout, stderr io.Writer) int {
	path, err := layerPath(name)
	if err != nil {
		fmt.Fprintln(stderr, "lectio:", err)
		return 2
	}
	layer, err := caldata.LoadLayer(path, name)
	if err != nil {
		fmt.Fprintln(stderr, "lectio:", err)
		return 1
	}
	slugs := make([]string, 0, len(layer.Cels))
	for s := range layer.Cels {
		slugs = append(slugs, s)
	}
	sort.Strings(slugs)

	problems := 0
	report := func(slug, msg string) {
		problems++
		fmt.Fprintf(stdout, "  [%s] %s\n", slug, msg)
	}
	for _, slug := range slugs {
		f := layer.Cels[slug].Fields
		if f["suppress"] == "true" {
			continue // a removal needs no other fields
		}
		switch d := f["date"]; {
		case d == "":
			report(slug, "missing date")
		case !calendar.ValidDate(calendar.DateSpec(d)):
			report(slug, fmt.Sprintf("unparseable date %q", d))
		}
		if r := f["rank"]; r != "" && !validRanks[r] {
			report(slug, fmt.Sprintf("unknown rank %q (want solemnity|feast|memorial|optional)", r))
		}
		if c := f["colour"]; c != "" && calendar.ParseColour(c) == "" {
			report(slug, fmt.Sprintf("unknown colour %q", c))
		}
		if cl := f["class"]; cl != "" && cl != "lord" && cl != "bvm" && cl != "saint" {
			report(slug, fmt.Sprintf("unknown class %q (want lord|bvm|saint)", cl))
		}
	}
	if problems > 0 {
		fmt.Fprintf(stderr, "lectio: %s: %d problem(s)\n", path, problems)
		return 1
	}
	fmt.Fprintf(stdout, "ok: %s (%d celebrations)\n", path, len(layer.Cels))
	return 0
}