diff options
| author | Danilo M. <danix@danix.xyz> | 2026-09-17 09:00:40 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-09-17 09:00:40 +0200 |
| commit | e6c5c5e6443345e019549d8b41ffa0c19d050c9f (patch) | |
| tree | 3ccb4be91e079954086ddb8835d21ecc86716663 | |
| parent | e9dd31b04324d9a95acf6561a4b6ebcee9d3b8c0 (diff) | |
| download | conky-theme-udt-e6c5c5e6443345e019549d8b41ffa0c19d050c9f.tar.gz conky-theme-udt-e6c5c5e6443345e019549d8b41ffa0c19d050c9f.zip | |
feat: parse the cached OWM response
Lua patterns instead of a JSON library: the current-weather response
is flat and known, so six scalars do not justify a dependency. Every
failure path (truncated write, empty file, garbage, an API error body,
nil input) returns nil rather than raising, since a Lua error here is
a blank dashboard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| -rw-r--r-- | lib/weather.lua | 41 | ||||
| -rw-r--r-- | test/fixtures/weather.json | 1 | ||||
| -rw-r--r-- | test/fixtures/weather_truncated.json | 1 | ||||
| -rw-r--r-- | test/test_weather.lua | 43 |
4 files changed, 86 insertions, 0 deletions
diff --git a/lib/weather.lua b/lib/weather.lua index 84f355a..a66d79a 100644 --- a/lib/weather.lua +++ b/lib/weather.lua @@ -142,4 +142,45 @@ function M.is_day(now, sunrise, sunset) return now >= sunrise and now <= sunset end +-- Parse the cached OWM response. +-- +-- Lua patterns, not a JSON library: the current-weather response is flat and +-- its shape is known, so six scalars do not justify a dependency. Returns nil +-- on anything unreadable, never an error. +function M.parse(src) + if type(src) ~= 'string' or src == '' then return nil end + + -- Scalars are matched inside their own object where the key would otherwise + -- be ambiguous. "temp" appears as a prefix of "temp_min" and "temp_max", so + -- it is anchored to the character that follows it. + local function num(pat) + return tonumber(src:match(pat)) + end + + local main = src:match('"main"%s*:%s*(%b{})') or '' + local wind = src:match('"wind"%s*:%s*(%b{})') or '' + local sys = src:match('"sys"%s*:%s*(%b{})') or '' + local cond = src:match('"weather"%s*:%s*%[%s*(%b{})') or '' + + local w = { + id = tonumber(cond:match('"id"%s*:%s*(%-?%d+)')), + description = cond:match('"description"%s*:%s*"([^"]*)"'), + temp = tonumber(main:match('"temp"%s*:%s*(%-?[%d%.]+)')), + feels_like = tonumber(main:match('"feels_like"%s*:%s*(%-?[%d%.]+)')), + humidity = tonumber(main:match('"humidity"%s*:%s*(%d+)')), + wind_speed = tonumber(wind:match('"speed"%s*:%s*([%d%.]+)')), + wind_deg = tonumber(wind:match('"deg"%s*:%s*(%d+)')), + sunrise = tonumber(sys:match('"sunrise"%s*:%s*(%d+)')), + sunset = tonumber(sys:match('"sunset"%s*:%s*(%d+)')), + city = src:match('"name"%s*:%s*"([^"]*)"'), + dt = num('"dt"%s*:%s*(%d+)'), + } + + -- Without these the card has nothing to draw, and a partial card is worse + -- than an honest "no data". An API error body fails here too: it carries a + -- cod and a message, no weather. + if not (w.id and w.temp and w.sunrise and w.sunset) then return nil end + return w +end + return M diff --git a/test/fixtures/weather.json b/test/fixtures/weather.json new file mode 100644 index 0000000..12493b0 --- /dev/null +++ b/test/fixtures/weather.json @@ -0,0 +1 @@ +{"coord":{"lon":11.0,"lat":45.0},"weather":[{"id":501,"main":"Rain","description":"moderate rain","icon":"10d"}],"base":"stations","main":{"temp":18.34,"feels_like":18.11,"temp_min":16.67,"temp_max":19.44,"pressure":1014,"humidity":72},"visibility":10000,"wind":{"speed":4.12,"deg":230},"clouds":{"all":75},"dt":1789625000,"sys":{"type":2,"id":2004688,"country":"XX","sunrise":1789600000,"sunset":1789646000},"timezone":7200,"id":1,"name":"Example City","cod":200} diff --git a/test/fixtures/weather_truncated.json b/test/fixtures/weather_truncated.json new file mode 100644 index 0000000..eb0099a --- /dev/null +++ b/test/fixtures/weather_truncated.json @@ -0,0 +1 @@ +{"coord":{"lon":11.0,"lat":45.0},"weather":[{"id":501,"main":"Rain","descrip diff --git a/test/test_weather.lua b/test/test_weather.lua index 1d35748..4ea953f 100644 --- a/test/test_weather.lua +++ b/test/test_weather.lua @@ -153,4 +153,47 @@ assert(weather.is_day(2000, 1000, 2000) == true, 'sunset counts as day') assert(weather.is_day(500, 1000, 2000) == false, 'before dawn is night') assert(weather.is_day(2500, 1000, 2000) == false, 'after dusk is night') +-- === Parsing the cache ==================================================== +-- The parser reads scalars by key with Lua patterns rather than pulling in a +-- JSON library: the response is flat and known, and six numbers do not justify +-- a dependency. Caching the whole response still costs nothing, since a field +-- added later is already on disk. +local function read(path) + local f = assert(io.open(path, 'r')) + local s = f:read('*a') + f:close() + return s +end + +local w = weather.parse(read('test/fixtures/weather.json')) +assert(w, 'the fixture must parse') +assert(w.id == 501, 'condition id, got ' .. tostring(w.id)) +assert(w.description == 'moderate rain', 'description, got ' .. tostring(w.description)) +assert(w.temp == 18.34, 'temp, got ' .. tostring(w.temp)) +assert(w.feels_like == 18.11, 'feels like, got ' .. tostring(w.feels_like)) +assert(w.humidity == 72, 'humidity, got ' .. tostring(w.humidity)) +assert(w.wind_speed == 4.12, 'wind m/s, got ' .. tostring(w.wind_speed)) +assert(w.wind_deg == 230, 'wind direction, got ' .. tostring(w.wind_deg)) +assert(w.sunrise == 1789600000, 'sunrise, got ' .. tostring(w.sunrise)) +assert(w.sunset == 1789646000, 'sunset, got ' .. tostring(w.sunset)) +assert(w.city == 'Example City', 'city, got ' .. tostring(w.city)) +assert(w.dt == 1789625000, 'observation time, got ' .. tostring(w.dt)) + +-- temp comes before feels_like in the response and both live under "main", +-- so a lazy pattern would read one for the other. They differ in the fixture +-- precisely so this is checkable. +assert(w.temp ~= w.feels_like, 'temp and feels_like must not collapse') + +-- Failure modes all return nil rather than raising: a Lua error here is a +-- blank dashboard, not a message. +assert(weather.parse(read('test/fixtures/weather_truncated.json')) == nil, + 'a truncated response must give nil') +assert(weather.parse('') == nil, 'empty input gives nil') +assert(weather.parse('not json at all') == nil, 'garbage gives nil') +assert(weather.parse(nil) == nil, 'nil input gives nil') + +-- An error body carries cod 401 and no weather. It must not parse as data. +assert(weather.parse('{"cod":401,"message":"Invalid API key."}') == nil, + 'an API error body must give nil') + print('test_weather: all assertions passed') |
