summaryrefslogtreecommitdiff
path: root/internal/tradlit
diff options
context:
space:
mode:
authorLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 10:32:28 +0200
committerLukasz Kasprzak <lukas@labunix.xyz>2026-07-24 10:32:28 +0200
commitc9f3bf46f0de09edb796e35f3dcff349febf75c9 (patch)
treec1a0b98b484e4da557c1ab205dfff0d92ce8ac36 /internal/tradlit
parenta865374755480a4aef2a6156afccdf08a91422ea (diff)
downloadlectio-c9f3bf46f0de09edb796e35f3dcff349febf75c9.tar.gz
lectio-c9f3bf46f0de09edb796e35f3dcff349febf75c9.zip
dayinfo: show feast/day name + colour (modern niedziela + traditional missalemeum) in cli/tui/web; v0.5.0
Diffstat (limited to 'internal/tradlit')
-rw-r--r--internal/tradlit/parse_test.go53
-rw-r--r--internal/tradlit/tradlit.go63
-rw-r--r--internal/tradlit/tradlit_test.go14
3 files changed, 112 insertions, 18 deletions
diff --git a/internal/tradlit/parse_test.go b/internal/tradlit/parse_test.go
index a7344cd..58cc66f 100644
--- a/internal/tradlit/parse_test.go
+++ b/internal/tradlit/parse_test.go
@@ -2,7 +2,10 @@ package tradlit
import (
"os"
+ "strings"
"testing"
+
+ "github.com/lukaszkasprzak/lectio/internal/liturgy"
)
func TestParse(t *testing.T) {
@@ -10,7 +13,7 @@ func TestParse(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- secs, err := Parse(body)
+ secs, _, err := Parse(body)
if err != nil {
t.Fatal(err)
}
@@ -33,3 +36,51 @@ func TestParse(t *testing.T) {
t.Errorf("missing parts: gospel=%v epistle=%v", gospel, epistle)
}
}
+
+// TestParseDayInfo checks the traditional (missalemeum) day-info
+// extraction against the fixture's "info" object: Name from info.title,
+// Season from info.tempora, Colour from info.colors[0] ("w" -> "white").
+func TestParseDayInfo(t *testing.T) {
+ body, err := os.ReadFile("testdata/2026-07-22.json")
+ if err != nil {
+ t.Fatal(err)
+ }
+ _, info, err := Parse(body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if info.Name != "St. Mary Magdalene" {
+ t.Errorf("Name = %q, want %q", info.Name, "St. Mary Magdalene")
+ }
+ if !strings.Contains(info.Season, "Pentecost") {
+ t.Errorf("Season = %q, want it to contain %q", info.Season, "Pentecost")
+ }
+ if info.Colour != "white" {
+ t.Errorf("Colour = %q, want %q", info.Colour, "white")
+ }
+}
+
+// TestParseDayInfoMissingInfo checks that a response with no (or empty)
+// info object yields a zero DayInfo rather than an error.
+func TestParseDayInfoMissingInfo(t *testing.T) {
+ _, info, err := Parse([]byte(`[{"sections":[]}]`))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if info != (liturgy.DayInfo{}) {
+ t.Errorf("info = %+v, want zero value for a response with no info object", info)
+ }
+}
+
+// TestParseDayInfoUnknownColourCode checks an unrecognised colour code maps
+// to "" rather than passing the raw code through.
+func TestParseDayInfoUnknownColourCode(t *testing.T) {
+ body := []byte(`[{"info":{"title":"Test","tempora":"","colors":["z"]},"sections":[]}]`)
+ _, info, err := Parse(body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if info.Colour != "" {
+ t.Errorf("Colour = %q, want empty for unknown code %q", info.Colour, "z")
+ }
+}
diff --git a/internal/tradlit/tradlit.go b/internal/tradlit/tradlit.go
index a7b53bc..b464e56 100644
--- a/internal/tradlit/tradlit.go
+++ b/internal/tradlit/tradlit.go
@@ -34,29 +34,66 @@ var citationRe = regexp.MustCompile(`\*([^*\n]+)\*`)
// apiResponse mirrors the shape of the missalemeum proper-of-the-day API:
// a single-element list of { info, sections }.
type apiResponse struct {
- Info struct {
- Title string `json:"title"`
- } `json:"info"`
+ Info apiInfo `json:"info"`
Sections []apiSection `json:"sections"`
}
+// apiInfo mirrors the missalemeum API's "info" object: the day's celebration
+// title, its temporal context ("tempora"), and its liturgical colour code(s)
+// -- see dayInfoFromAPI, which turns it into a liturgy.DayInfo.
+type apiInfo struct {
+ Title string `json:"title"`
+ Tempora string `json:"tempora"`
+ Colors []string `json:"colors"`
+}
+
type apiSection struct {
ID string `json:"id"`
Label string `json:"label"`
Body [][]string `json:"body"`
}
+// tradColours maps missalemeum's single-letter liturgical colour codes to
+// DayInfo's normalized colour names; a code not listed here (or an empty
+// Colors list) leaves DayInfo.Colour "".
+var tradColours = map[string]string{
+ "w": "white",
+ "r": "red",
+ "v": "violet",
+ "g": "green",
+ "p": "rose",
+}
+
+// dayInfoFromAPI turns a response's info object into a liturgy.DayInfo:
+// Title -> Name, Tempora -> Season, and the first Colors code -> Colour via
+// tradColours (unknown/missing -> ""). A zero-value apiInfo (no "info" key
+// in the response) yields a zero DayInfo.
+func dayInfoFromAPI(info apiInfo) liturgy.DayInfo {
+ colour := ""
+ if len(info.Colors) > 0 {
+ colour = tradColours[strings.ToLower(info.Colors[0])]
+ }
+ return liturgy.DayInfo{
+ Name: info.Title,
+ Season: info.Tempora,
+ Colour: colour,
+ }
+}
+
// Parse decodes a missalemeum proper-of-the-day API response body into
-// liturgy.Sections. Sections with an empty id or empty body are skipped.
-func Parse(jsonBody []byte) ([]liturgy.Section, error) {
+// liturgy.Sections plus the day's liturgical identity (its "info" object,
+// see dayInfoFromAPI). Sections with an empty id or empty body are skipped.
+func Parse(jsonBody []byte) ([]liturgy.Section, liturgy.DayInfo, error) {
var resp []apiResponse
if err := json.Unmarshal(jsonBody, &resp); err != nil {
- return nil, fmt.Errorf("tradlit: parse: %w", err)
+ return nil, liturgy.DayInfo{}, fmt.Errorf("tradlit: parse: %w", err)
}
if len(resp) == 0 {
- return nil, fmt.Errorf("tradlit: parse: empty response")
+ return nil, liturgy.DayInfo{}, fmt.Errorf("tradlit: parse: empty response")
}
+ info := dayInfoFromAPI(resp[0].Info)
+
var out []liturgy.Section
for _, sec := range resp[0].Sections {
if sec.ID == "" || len(sec.Body) == 0 || len(sec.Body[0]) == 0 {
@@ -85,7 +122,7 @@ func Parse(jsonBody []byte) ([]liturgy.Section, error) {
Paragraphs: [][]string{lines},
})
}
- return out, nil
+ return out, info, nil
}
// cachePath returns where Load caches a (date, lang) day's raw API response:
@@ -108,7 +145,7 @@ func cachePath(date, lang string) string {
// the cache path (best-effort -- a cache-write failure never fails the
// request) before being parsed. On HTTP 404 (no propers published for
// that date) it returns a clear error and writes nothing to the cache.
-func Load(date, lang string, offline bool) ([]liturgy.Section, error) {
+func Load(date, lang string, offline bool) ([]liturgy.Section, liturgy.DayInfo, error) {
if offline {
return loadCached(date, lang)
}
@@ -116,20 +153,20 @@ func Load(date, lang string, offline bool) ([]liturgy.Section, error) {
}
// loadCached implements Load's offline path.
-func loadCached(date, lang string) ([]liturgy.Section, error) {
+func loadCached(date, lang string) ([]liturgy.Section, liturgy.DayInfo, error) {
body, err := os.ReadFile(cachePath(date, lang))
if err != nil {
- return nil, fmt.Errorf("tradlit: no cached traditional propers for %s (%s); view it online or run 'lectio update' first", date, lang)
+ return nil, liturgy.DayInfo{}, fmt.Errorf("tradlit: no cached traditional propers for %s (%s); view it online or run 'lectio update' first", date, lang)
}
return Parse(body)
}
// loadLive implements Load's online path: fetch, best-effort cache the raw
// body, then parse.
-func loadLive(date, lang string) ([]liturgy.Section, error) {
+func loadLive(date, lang string) ([]liturgy.Section, liturgy.DayInfo, error) {
body, err := fetch(date, lang)
if err != nil {
- return nil, err
+ return nil, liturgy.DayInfo{}, err
}
writeCache(date, lang, body)
return Parse(body)
diff --git a/internal/tradlit/tradlit_test.go b/internal/tradlit/tradlit_test.go
index cd2cc83..8aa46eb 100644
--- a/internal/tradlit/tradlit_test.go
+++ b/internal/tradlit/tradlit_test.go
@@ -32,13 +32,16 @@ func TestLoadOnlineCachesRawBody(t *testing.T) {
defer func() { baseURL = orig }()
t.Setenv("XDG_CACHE_HOME", t.TempDir())
- secs, err := Load("2026-07-22", "en", false)
+ secs, info, err := Load("2026-07-22", "en", false)
if err != nil {
t.Fatalf("Load: %v", err)
}
if len(secs) == 0 {
t.Fatal("Load returned no sections")
}
+ if info.Name != "St. Mary Magdalene" {
+ t.Errorf("Load DayInfo.Name = %q, want %q", info.Name, "St. Mary Magdalene")
+ }
if hits != 1 {
t.Errorf("server hit %d times, want 1", hits)
}
@@ -66,7 +69,7 @@ func TestLoadOnline404WritesNoCache(t *testing.T) {
defer func() { baseURL = orig }()
t.Setenv("XDG_CACHE_HOME", t.TempDir())
- _, err := Load("2026-07-22", "en", false)
+ _, _, err := Load("2026-07-22", "en", false)
if err == nil {
t.Fatal("expected error on 404, got nil")
}
@@ -95,13 +98,16 @@ func TestLoadOfflineReadsCache(t *testing.T) {
t.Fatal(err)
}
- secs, err := Load("2026-07-22", "pl", true)
+ secs, info, err := Load("2026-07-22", "pl", true)
if err != nil {
t.Fatalf("Load offline: %v", err)
}
if len(secs) == 0 {
t.Fatal("Load offline returned no sections")
}
+ if info.Name != "St. Mary Magdalene" {
+ t.Errorf("Load offline DayInfo.Name = %q, want %q", info.Name, "St. Mary Magdalene")
+ }
}
// TestLoadOfflineMissingCacheErrors checks Load's offline path errors
@@ -110,7 +116,7 @@ func TestLoadOfflineReadsCache(t *testing.T) {
func TestLoadOfflineMissingCacheErrors(t *testing.T) {
t.Setenv("XDG_CACHE_HOME", t.TempDir())
- _, err := Load("2026-07-22", "pl", true)
+ _, _, err := Load("2026-07-22", "pl", true)
if err == nil {
t.Fatal("expected error for missing cache, got nil")
}