From c9f3bf46f0de09edb796e35f3dcff349febf75c9 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Fri, 24 Jul 2026 10:32:28 +0200 Subject: dayinfo: show feast/day name + colour (modern niedziela + traditional missalemeum) in cli/tui/web; v0.5.0 --- internal/tradlit/parse_test.go | 53 ++++++++++++++++++++++++++++++++- internal/tradlit/tradlit.go | 63 +++++++++++++++++++++++++++++++--------- internal/tradlit/tradlit_test.go | 14 ++++++--- 3 files changed, 112 insertions(+), 18 deletions(-) (limited to 'internal/tradlit') 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") } -- cgit v1.3