From fa0cccade064f1b6ef90709d7694f9f70e298291 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 3 Feb 2026 13:50:47 +0000 Subject: [PATCH] docs(claude): update CLAUDE.md to match current codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix stale fragment rendering example (into_request → into_request_and_search) - Correct domain conversion convention to to_domain() and fold into Repository Pattern - Add listing.rs and missing ID types to architecture tree - Document define_enriched_get_handler! macro - Add Route Module Structure pattern (load/page/fragment trio, build_page_view) - Note submission types vs direct domain structs in FlexiblePayload --- CLAUDE.md | 65 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 99ee48f..81b2fe5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,9 +56,10 @@ The codebase follows **Clean Architecture / Domain-Driven Design** with four lay src/ ├── domain/ # Pure business logic, no external dependencies │ ├── errors.rs # RepositoryError enum -│ ├── ids.rs # Typed ID wrappers (RoasterId, RoastId, BagId) +│ ├── ids.rs # Typed ID wrappers (RoasterId, RoastId, BagId, BrewId, GearId, etc.) +│ ├── listing.rs # Pagination & sorting (SortKey, ListRequest, Page, PageSize) │ ├── repositories.rs # Repository traits -│ └── {entity}.rs # Entity definitions (roasters, roasts, bags, etc.) +│ └── {entity}.rs # Entity definitions (roasters, roasts, bags, brews, gear, etc.) │ ├── infrastructure/ # External integrations (database, HTTP client) │ ├── repositories/ # SQL implementations of repository traits @@ -91,7 +92,13 @@ pub trait RoasterRepository { } ``` -SQL implementations live in `infrastructure/repositories/`. +SQL implementations live in `infrastructure/repositories/`. Each uses a private `Record` struct (e.g., `BagRecord`) with a `to_domain()` method to convert from database row to domain entity: + +```rust +impl BagRecord { + fn to_domain(self) -> Bag { ... } +} +``` ### Typed IDs @@ -180,11 +187,11 @@ When a Datastar request is detected, return a fragment instead of a full page: ```rust pub(crate) async fn roasters_page(...) -> Result { - let request = query.into_request::(); + let (request, search) = query.into_request_and_search::(); if is_datastar_request(&headers) { // Datastar request → return fragment only - return render_roaster_list_fragment(state, request, is_authenticated).await; + return render_roaster_list_fragment(state, request, search, is_authenticated).await; } // Traditional request → return full page with layout @@ -254,11 +261,12 @@ navigator.sort_href(key) // "/roasters?sort=name&dir=..." #### Flexible Payload Handling -Handlers accept both JSON and form data via `FlexiblePayload`: +Handlers accept both JSON and form data via `FlexiblePayload`. When form fields don't map directly to the domain `New*` struct (e.g., the form sends a roaster name that needs to be resolved to an ID), use a `*Submission` newtype that handles the conversion: ```rust pub(crate) async fn create_roaster( - payload: FlexiblePayload, + payload: FlexiblePayload, // simple — form maps 1:1 to domain + // or: FlexiblePayload // submission type — needs conversion ) -> Result { let (new_roaster, source) = payload.into_parts(); @@ -277,11 +285,14 @@ pub(crate) async fn create_roaster( For simple get/delete API handlers, use the macros in `application/routes/macros.rs`: ```rust -use super::macros::{define_get_handler, define_delete_handler}; +use super::macros::{define_get_handler, define_enriched_get_handler, define_delete_handler}; // GET /api/v1/roasters/:id → returns JSON define_get_handler!(get_roaster, RoasterId, Roaster, roaster_repo); +// GET with enriched data (joins related entities) → returns JSON +define_enriched_get_handler!(get_roast, RoastId, RoastWithRoaster, roast_repo, get_with_roaster); + // DELETE /api/v1/roasters/:id → returns fragment for Datastar or 204 for API define_delete_handler!( delete_roaster, @@ -292,22 +303,40 @@ define_delete_handler!( ); ``` +### Route Module Structure + +Each list-bearing route module (roasters, roasts, bags, gear, brews) follows the same internal structure: + +```rust +// Path constants for full-page and fragment URLs +const ROASTER_PAGE_PATH: &str = "/roasters"; +const ROASTER_FRAGMENT_PATH: &str = "/roasters#roaster-list"; + +// Data loader — calls repo.list() and builds view models via build_page_view() +async fn load_roaster_page(state, request, search) + -> Result<(Paginated, ListNavigator), AppError> + +// Page handler — checks is_datastar_request(), returns full page or fragment +pub(crate) async fn roasters_page(...) -> Result + +// Fragment renderer — returns just the list partial for Datastar replacement +async fn render_roaster_list_fragment(state, request, search, is_authenticated) + -> Result +``` + +The `build_page_view()` helper in `application/routes/support.rs` standardises the conversion from a repo `Page` to `(Paginated, ListNavigator)`: + +```rust +let (items, navigator) = build_page_view(page, request, RoasterView::from, + ROASTER_PAGE_PATH, ROASTER_FRAGMENT_PATH, search); +``` + ### Error Handling - Domain errors: `RepositoryError` in `domain/errors.rs` - HTTP errors: `AppError` in `application/errors.rs` with proper status code mapping - CLI errors: Use `anyhow::Result` for simplicity -### Domain Conversion - -Records from the database should have an `into_domain()` method or equivalent: - -```rust -impl BagRecord { - fn into_domain(self) -> Bag { ... } -} -``` - ## Table & List Patterns ### Template Structure