Skip to content

Add API endpoints for Home Assistant integration#271

Open
dubadub wants to merge 9 commits intomainfrom
ha-api-endpoints
Open

Add API endpoints for Home Assistant integration#271
dubadub wants to merge 9 commits intomainfrom
ha-api-endpoints

Conversation

@dubadub
Copy link
Member

@dubadub dubadub commented Mar 4, 2026

Summary

  • Add GET /api/stats endpoint returning recipe and menu counts
  • Add GET /api/menus endpoint listing all .menu files
  • Add GET /api/menus/*path endpoint returning parsed menu with sections, meals, date/time extraction via regex
  • Add GET /api/pantry/expiring?days=N endpoint returning items expiring within N days
  • Add GET /api/pantry/depleted endpoint returning low-stock pantry items
  • Include research, design, and implementation plan docs

These endpoints power the homeassistant-cookcli HACS custom component, which provides Calendar (meal plans from .menu files), Todo (shopping list), and Sensor entities for Home Assistant.

Menu calendar mapping

Menu sections with dates in parentheses (== Day 1 (2026-03-04) ==) are extracted via regex and mapped to HA calendar events. Meal type headers with times (Breakfast (08:30):) provide event start times. Sections without dates are ignored for calendar purposes but still included in shopping lists.

Test plan

  • cargo fmt passes
  • cargo clippy passes
  • cargo test passes
  • Manual test: cargo run -- server ./seed then curl http://localhost:9080/api/stats returns {"recipe_count":N,"menu_count":N}
  • Manual test: curl http://localhost:9080/api/menus returns list of menu files
  • Manual test: curl http://localhost:9080/api/menus/2%20Day%20Plan.menu returns parsed menu JSON
  • Manual test: curl http://localhost:9080/api/pantry/expiring?days=30 returns expiring items
  • Manual test: curl http://localhost:9080/api/pantry/depleted returns depleted items

dubadub added 8 commits March 4, 2026 17:33
Add a new stats API endpoint that returns the total number of recipes
and menus by recursively walking the recipe tree. This is the first
of several endpoints needed for Home Assistant integration.
Adds a new API endpoint that recursively walks the recipe tree and
returns all .menu files with their name and relative path, enabling
Home Assistant to discover available menu files for config flow.
Add a handler that parses .menu files and returns structured JSON with
sections grouped by meal type. Extracts dates from section names and
times from meal headers via regex. This endpoint powers the HA Calendar
entity by providing structured meal plan data.
Add GET /api/pantry/expiring?days=N and GET /api/pantry/depleted
endpoints for Home Assistant integration. The expiring endpoint
returns items expiring within N days (default 7) with days_remaining.
The depleted endpoint returns items flagged as low stock via is_low().
@claude
Copy link

claude bot commented Mar 4, 2026

Code Review: Add API endpoints for Home Assistant integration

Thanks for this PR — the HA integration is a great use case and the overall approach is solid. Here are my findings across quality, correctness, and a few design concerns.


Blocking Issues

1. Blocking I/O in async handlers (pantry.rs)

get_expiring, get_depleted, and get_pantry all use std::fs::read_to_string inside async handlers. This blocks the Tokio runtime thread and can cause latency spikes under load. Switch to tokio::fs::read_to_string(...).await instead.

2. Duplicate pantry parsing logic

get_expiring and get_depleted each independently read + parse the pantry file, duplicating ~20 lines. Extract a shared async helper (e.g., load_pantry(state) -> Result<PantryConf, ...>) and call it from both handlers.

3. Negative days not validated

get_expiring accepts ?days=N but doesn't reject negative values. days=-30 would silently return items that expired a month ago. Add a guard that returns 400 Bad Request when days < 0.


Design Concerns

4. Full filesystem tree walk on every request

Both /api/stats and /api/menus call cooklang_find::build_tree on every request. With HA polling every 5 minutes this runs continuously. Consider caching the tree in AppState (invalidated via the existing /api/reload mechanism), following the pattern used elsewhere in the server.

5. /api/stats response is missing fields from the design doc

docs/plans/2026-03-04-homeassistant-integration-design.md specifies pantry_item_count, pantry_expiring_count, and pantry_depleted_count in the stats response. The implementation only returns recipe_count and menu_count. If the HA component depends on the fuller shape it will encounter missing fields at runtime.

6. Route ordering: /menus vs /menus/*path

In src/server/mod.rs, /menus/*path is registered before /menus. Axum resolves exact paths before wildcards so it works, but it's non-obvious. Place /menus before /menus/*path to match natural reading order.


Code Quality

7. Duplicated json_error helper

json_error is defined independently in both stats.rs and menus.rs. Move it to a shared location (e.g., a util submodule in handlers/) to avoid the definitions drifting apart.

8. Plain-text meal items silently dropped

In get_menu, LineItem::Text inside a meal is silently ignored with a brief comment. If free-text notes in menu files are intentionally excluded from the API response, add a doc comment explaining the decision so future maintainers understand the intent.

9. Check for unused scale param on list_menus

list_menus appears to share Query<MenuQuery> which includes a scale field. If list_menus doesn't use scale, that's dead API surface — use a separate empty query struct or remove the extractor from that handler.


Test Coverage

The regex-heavy helpers (extract_date, extract_time, extract_meal_type, is_meal_header) are prime candidates for unit tests given how critical correct date/time extraction is for the HA calendar integration. Suggested cases:

  • "== Day 1 (2026-03-04) =="Some("2026-03-04")
  • "== Extras =="None
  • "Breakfast (08:30):" → type "Breakfast", time Some("08:30")
  • "Dinner:" → type "Dinner", time None
  • ?days=-1 → 400 Bad Request

Documentation

The three plan/design/research docs total ~2900 lines and contain implementation-specific local paths (/Users/alexeydubovskoy/...), Python file stubs, and step-by-step commit instructions. This content belongs in the homeassistant-cookcli repo, not here. Before merging, either trim to a concise architecture overview or remove the verbose plan files from this PR.


Summary: The implementation is functionally correct and the API shape is well-considered. The three most important fixes before merge are: (1) switch to async-safe file I/O in pantry handlers, (2) deduplicate the pantry parsing logic, and (3) validate the days parameter. The missing stats fields and the oversized plan docs are also worth addressing.

@cooklang cooklang deleted a comment from claude bot Mar 5, 2026
@cooklang cooklang deleted a comment from claude bot Mar 5, 2026
@cooklang cooklang deleted a comment from claude bot Mar 5, 2026
@cooklang cooklang deleted a comment from claude bot Mar 5, 2026
- Switch pantry handlers from std::fs to tokio::fs for async I/O
- Extract shared load_pantry() helper to deduplicate pantry parsing
- Validate days parameter rejects negative values (400 Bad Request)
- Add pantry_item_count, pantry_expiring_count, pantry_depleted_count to /api/stats
- Move json_error and check_path to shared common.rs module
- Reorder /menus before /menus/*path for clarity
- Add doc comment explaining why plain text items are excluded from menu API
@claude
Copy link

claude bot commented Mar 5, 2026

Code Review: Add API endpoints for Home Assistant integration

Overall this is a well-structured PR that adds useful endpoints cleanly. The code is readable, error handling is consistent, and the path-traversal protection is a good security touch. A few things worth addressing before merge:


Bugs / Correctness

1. stats.rs hardcodes the expiring threshold at 7 days

src/server/handlers/stats.rs:188-189 computes pantry_expiring_count using a hardcoded 7-day window, regardless of what a caller might expect. The /api/pantry/expiring?days=N endpoint respects the days parameter, but /api/stats will always report "items expiring in 7 days". This inconsistency will surprise HA users who configure a different window.

Consider extracting the counting logic into a shared helper that both stats and get_expiring call, or document the fixed window in the API.

2. add_item silently swallows pantry parse errors

// pantry.rs:2856
let mut pantry_conf = load_pantry(&state).await.unwrap_or_default();

If load_pantry fails (e.g. the file exists but is malformed), add_item creates a fresh empty pantry and overwrites the file. This differs from remove_item and update_item which correctly propagate the error. A corrupted pantry could be silently replaced with an empty one.

This is actually the same behavior as the pre-PR code (result.output().cloned().unwrap_or_default()), so it's not a regression — but now that there's a shared load_pantry() helper it's worth fixing consistently across all three mutation handlers.


Code Quality

3. .clone() before unwrap_or_else on Option<String>

// menus.rs (collect_menus)
let name = entry.name().clone().unwrap_or_else(|| relative.to_string());

If entry.name() returns Option<&String> (or Option<String>), .clone() before unwrap_or_else is either a no-op or clones an owned value unnecessarily. The idiomatic Rust spelling is .cloned().unwrap_or_else(...) for Option<&T>, or just .unwrap_or_else(...) if it's already owned.

4. stats.rs duplicates pantry-parsing logic

The pantry-counting block in stats (~30 lines) re-implements the logic already in get_expiring and get_depleted. Now that load_pantry() is shared and parse_date is pub, the stats handler could call those functions directly or a thin helper that returns counts. This would keep the expiry logic in one place and fix issue #1 at the same time.

5. parse_date has ambiguous format ordering

// pantry.rs
let formats = [
    "%Y-%m-%d", "%d.%m.%Y", "%d/%m/%Y", "%m/%d/%Y", ...
];

%d/%m/%Y and %m/%d/%Y are both attempted for slash-separated dates. For a date like 01/02/2026, the first matching format (%d/%m/%Y) wins, giving Feb 1. A US-locale user writing 01/02/2026 to mean Jan 2 would get the wrong date silently. Consider documenting that D/M/Y takes precedence, or requiring ISO 8601 (%Y-%m-%d) only in pantry files to avoid the ambiguity.


Performance

6. Tree is rebuilt on every request

cooklang_find::build_tree() is called fresh in stats, list_menus, and get_menu. For large recipe collections (hundreds of files) this is a full filesystem walk per API call. The existing recipe handlers likely have the same issue, but these new endpoints (especially /api/stats) are designed to be polled every 5 minutes by HA's coordinator — making it more visible.

Not a blocker, but worth a follow-up issue to add caching (e.g. an Arc<RwLock<RecipeTree>> in AppState with a TTL or file-watch invalidation).


Minor / Nits

7. Large planning docs committed to docs/plans/

Three multi-hundred-line design docs totalling ~2300 lines are included in docs/plans/. This is useful context but might be better suited to a GitHub wiki page or PR description rather than the repository itself, to avoid inflating git log and git blame noise for future contributors. Up to maintainer preference.

8. /api/menus/*path/shopping described but not implemented

The design doc describes a GET /api/menus/*path/shopping endpoint for aggregated shopping lists. It's fine to defer, but the PR description doesn't mention the omission — worth a note or a follow-up issue so it doesn't get lost.


What's done well

  • LazyLock for compiled regexes — correct and efficient.
  • check_path path-traversal guard using Utf8Component::Normal is a solid security choice.
  • Extracting load_pantry() and get_pantry_path() eliminates substantial duplication from the original pantry.rs.
  • Switching from std::fs to tokio::fs is the right move in an async handler.
  • Negative days validation returning 400 is good API hygiene.
  • Sorting expiring items by days_remaining so the most urgent items come first is a nice UX touch.
  • serde(tag = "kind") on MenuMealItem produces clean, discriminated JSON for the HA consumer.

The main ask before merge is addressing the stats hardcoded threshold (#1) and the add_item silent-overwrite risk (#2). The rest are suggestions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant