feat: Phase 10e — data layer models and API repository #31

Merged
bjoern merged 1 commit from feat/flutter-data-models into main 2026-06-30 00:05:47 +02:00
Member

Phase 10e: Data Layer and API Models

Freezed models matching the backend JSON envelopes, Dart enums matching the C# wire format, and the DoujinApiRepository implementation.

Key Finding: Enum Wire Format

The backend (ASP.NET Core default JSON serializer) serializes enums as integers ("readingDirection": 1, "kind": 0), not strings. Sending string enums results in 500 errors. All Dart enum values use @JsonValue(int) annotations to match the exact wire representation. This was empirically verified by running the backend and capturing the HTTP response.

What's included

Enums (domain/entities/enums.dart):

  • ReadingDirection, TitleKind, VariantKind, PersonRole — @JsonValue(int) for integer wire format

Envelope models (data/models/envelope.dart):

  • ResourceResponse<T>, CollectionResponse<T> — generic, genericArgumentFactories: true
  • HypermediaLink, HypermediaAction, PageInfo

Domain models (data/models/doujin_models.dart) — 12 freezed models:

  • DoujinSummary, DoujinDetail, Title, VariantSummary, VariantDetail, Chapter, Page, Tag, Person, Circle, DoujinPerson, SearchResult
  • All match backend JSON property names, GUIDs as String, timestamps as DateTime

SearchQuery (data/models/search_query.dart):

  • Request body builder for POST /api/doujins/search

DoujinRepository (domain/repositories/doujin_repository.dart):

  • Interface: listDoujins, getDoujin, search + Paginated<T> typedef

DoujinApiRepository (data/repositories/doujin_api_repository.dart):

  • Dio-backed implementation parsing hypermedia envelopes into domain models

Verification

  • flutter analyze: No issues found ✅
  • flutter test: 56/56 passed (34 existing + 22 new) ✅
## Phase 10e: Data Layer and API Models Freezed models matching the backend JSON envelopes, Dart enums matching the C# wire format, and the DoujinApiRepository implementation. ### Key Finding: Enum Wire Format The backend (ASP.NET Core default JSON serializer) serializes enums as **integers** (`"readingDirection": 1`, `"kind": 0`), not strings. Sending string enums results in 500 errors. All Dart enum values use `@JsonValue(int)` annotations to match the exact wire representation. This was **empirically verified** by running the backend and capturing the HTTP response. ### What's included **Enums** (`domain/entities/enums.dart`): - `ReadingDirection`, `TitleKind`, `VariantKind`, `PersonRole` — `@JsonValue(int)` for integer wire format **Envelope models** (`data/models/envelope.dart`): - `ResourceResponse<T>`, `CollectionResponse<T>` — generic, `genericArgumentFactories: true` - `HypermediaLink`, `HypermediaAction`, `PageInfo` **Domain models** (`data/models/doujin_models.dart`) — 12 freezed models: - `DoujinSummary`, `DoujinDetail`, `Title`, `VariantSummary`, `VariantDetail`, `Chapter`, `Page`, `Tag`, `Person`, `Circle`, `DoujinPerson`, `SearchResult` - All match backend JSON property names, GUIDs as String, timestamps as DateTime **SearchQuery** (`data/models/search_query.dart`): - Request body builder for `POST /api/doujins/search` **DoujinRepository** (`domain/repositories/doujin_repository.dart`): - Interface: `listDoujins`, `getDoujin`, `search` + `Paginated<T>` typedef **DoujinApiRepository** (`data/repositories/doujin_api_repository.dart`): - Dio-backed implementation parsing hypermedia envelopes into domain models ### Verification - `flutter analyze`: **No issues found** ✅ - `flutter test`: **56/56 passed** (34 existing + 22 new) ✅
feat: Phase 10e — data layer models and API repository
All checks were successful
Flutter CI / analyze-and-test (pull_request) Successful in 50s
4186c53737
Domain enums (matching backend C# wire format):
- ReadingDirection, TitleKind, VariantKind, PersonRole
- @JsonValue(int) annotations — backend serializes enums as integers,
  not strings (empirically verified by running the backend)

Envelope models (freezed + genericArgumentFactories):
- ResourceResponse<T>, CollectionResponse<T> for hypermedia envelopes
- HypermediaLink, HypermediaAction, PageInfo

Domain models (freezed + json_serializable):
- DoujinSummary, DoujinDetail, Title, VariantSummary, VariantDetail,
  Chapter, Page, Tag, Person, Circle, DoujinPerson, SearchResult
- All match backend JSON property names exactly
- GUIDs as String, timestamps as DateTime, nullable fields as nullable

SearchQuery — request body builder for POST /api/doujins/search

DoujinRepository interface — listDoujins, getDoujin, search

DoujinApiRepository — Dio-backed implementation:
- GET /api/doujins?page={page}&pageSize={pageSize}
- GET /api/doujins/{id}
- POST /api/doujins/search
- Parses hypermedia envelopes into domain models + page info

Tests: 22 new (models + repository), 56 total pass
flutter analyze: 0 issues

Flutter Coverage

File Line coverage
lib/core/theme.dart 96.9% (31 of 32)
lib/domain/entities/stored_settings.dart 100.0% (6 of 6)
lib/presentation/state/actions.dart 100.0% (7 of 7)
lib/presentation/state/reducers.dart 100.0% (31 of 31)
lib/data/api_client.dart 88.9% (8 of 9)
lib/data/repositories/health_repository.dart 69.2% (18 of 26)
lib/data/models/doujin_models.dart 95.0% (19 of 20)
lib/data/models/doujin_models.g.dart 50.8% (101 of 199)
lib/data/models/envelope.dart 81.2% (13 of 16)
lib/data/models/envelope.g.dart 50.7% (34 of 67)
lib/data/models/search_query.dart 50.0% (2 of 4)
lib/data/models/search_query.g.dart 38.5% (15 of 39)
lib/app/app.dart 100.0% (17 of 17)
lib/app/store.dart 100.0% (10 of 10)
lib/presentation/pages/library/library_page.dart 100.0% (9 of 9)
lib/presentation/pages/settings/settings_page.dart 100.0% (86 of 86)
lib/presentation/middleware/epics.dart 100.0% (23 of 23)
lib/data/repositories/doujin_api_repository.dart 86.2% (25 of 29)

Total: 72.2% (455 of 630)

<!-- flutter-coverage-comment --> ## Flutter Coverage | File | Line coverage | |:---|---:| | lib/core/theme.dart | 96.9% (31 of 32) | | lib/domain/entities/stored_settings.dart | 100.0% (6 of 6) | | lib/presentation/state/actions.dart | 100.0% (7 of 7) | | lib/presentation/state/reducers.dart | 100.0% (31 of 31) | | lib/data/api_client.dart | 88.9% (8 of 9) | | lib/data/repositories/health_repository.dart | 69.2% (18 of 26) | | lib/data/models/doujin_models.dart | 95.0% (19 of 20) | | lib/data/models/doujin_models.g.dart | 50.8% (101 of 199) | | lib/data/models/envelope.dart | 81.2% (13 of 16) | | lib/data/models/envelope.g.dart | 50.7% (34 of 67) | | lib/data/models/search_query.dart | 50.0% (2 of 4) | | lib/data/models/search_query.g.dart | 38.5% (15 of 39) | | lib/app/app.dart | 100.0% (17 of 17) | | lib/app/store.dart | 100.0% (10 of 10) | | lib/presentation/pages/library/library_page.dart | 100.0% (9 of 9) | | lib/presentation/pages/settings/settings_page.dart | 100.0% (86 of 86) | | lib/presentation/middleware/epics.dart | 100.0% (23 of 23) | | lib/data/repositories/doujin_api_repository.dart | 86.2% (25 of 29) | **Total: 72.2% (455 of 630)**
Author
Member

🤖 Hermes automated review: minor comments

Reviewed head 4186c537 (13 new files, +6214 / −0). I cross-checked every hand-written Dart model against the backend C# DTOs (DoujinDtos.cs, MetadataDtos.cs, SearchDtos.cs, ResponseEnvelope.cs) and the C# enum definitions to verify the wire-format contract.

What was verified ✅

Wire-format contract (the critical part of this PR):

  • Enum integer ordinals match exactly. Dart @JsonValue(0..5) for ReadingDirection, TitleKind, VariantKind, PersonRole all align with the C# enum declarations (Artist=0 … Other=5, etc.).
  • Integer serialization confirmed. EnvelopeJsonOptions.Default registers no JsonStringEnumConverter, so System.Text.Json serializes enums as integers by default — the PR's central claim is correct. The PR description's note that sending string enums → 500 is consistent with this.
  • personRole / variantKind correctly typed String?. The backend SearchDoujinsDto declares these as string? (not the enum types), matching Dart. If these had been Dart enums, the search filter body would have serialized to integers and silently failed to match the backend's string parsing.
  • All JSON property names match the [JsonPropertyName] attributes on the C# DTOs (camelCase).
  • Envelope structure matches (data / page / links / actions, PageInfo fields).
  • GUID-as-String / DateTime round-trip is consistent with how ASP.NET Core serializes Guid and DateTime.

Repository implementation (doujin_api_repository.dart):

  • Correct Dio usage; no string-concatenated query strings (queryParameters map is used). No path-injection risk: getDoujin builds /api/doujins/$id and the backend route constraint /{id:guid} validates format at routing time (malformed id → 404).
  • Defensive PageInfo fallback when the envelope omits page is harmless — the backend always sets it (verified: every CollectionResponse construction across all 6 endpoint files passes a non-null PageInfo).

Static security scan: clean — no hardcoded secrets, shell injection, eval/exec, pickle, SQL injection, or insecure-transport patterns.

Minor non-blocking notes

  1. listDoujins / search totalItems fallback is imprecise (doujin_api_repository.dart:20, :69). When envelope.page is null, the synthesized PageInfo uses totalItems: envelope.data.length. This is the current page size, not the true total. Since the backend always populates page, this never triggers in practice — but if it ever did, the UI would show "page 1 of 1" for a multi-page result. Low risk; consider documenting as a fallback-only path or logging when it occurs.

  2. doujin_models.g.dart / envelope.g.dart coverage is low (50.8% / 50.7% per CI). This is expected for generated fromJson/toJson code (the _$XFromJson constructors have many branches for nullable/defaulted fields). Not actionable, just noting it explains the 72.2% total.

  3. SearchQuery.toJson() includes null fields (search_query.dart). The test models_test.dart asserts json.containsKey('excludeTags') is true for unset fields. This is harmless (backend ignores nulls) and arguably more debuggable, but it diverges from a "sparse PATCH" style. The backend SearchDoujinsDto treats null filters as "no filter" either way, so no correctness impact.

No security issues. No logic errors. No blocking findings.

Verification

CI/CD: Flutter coverage comment #491 is current for head 4186c537 (72.2%, 455/630 lines) and was generated at 23:57:34Z — matching the PR head. The flutter-ci.yml job posts coverage only on a successful flutter analyze + flutter test, which the PR body confirms (56/56 passed: 34 existing + 22 new). Local build/test skipped per CI-evidence policy.

Note: backend coverage comment was not posted for this PR (head branch is Flutter-only changes), which is expected — the C# backend is unchanged here.


Automated daily review. I never merge PRs. This is a PR conversation comment with file references; it is not a formal Forgejo review-approval state (the current MCP integration cannot create those).

## 🤖 Hermes automated review: minor comments Reviewed head `4186c537` (13 new files, +6214 / −0). I cross-checked every hand-written Dart model against the backend C# DTOs (`DoujinDtos.cs`, `MetadataDtos.cs`, `SearchDtos.cs`, `ResponseEnvelope.cs`) and the C# enum definitions to verify the wire-format contract. ### What was verified ✅ **Wire-format contract (the critical part of this PR):** - **Enum integer ordinals match exactly.** Dart `@JsonValue(0..5)` for `ReadingDirection`, `TitleKind`, `VariantKind`, `PersonRole` all align with the C# enum declarations (`Artist=0 … Other=5`, etc.). - **Integer serialization confirmed.** `EnvelopeJsonOptions.Default` registers **no** `JsonStringEnumConverter`, so System.Text.Json serializes enums as integers by default — the PR's central claim is correct. The PR description's note that sending string enums → 500 is consistent with this. - **`personRole` / `variantKind` correctly typed `String?`.** The backend `SearchDoujinsDto` declares these as `string?` (not the enum types), matching Dart. If these had been Dart enums, the search filter body would have serialized to integers and silently failed to match the backend's string parsing. - **All JSON property names match** the `[JsonPropertyName]` attributes on the C# DTOs (camelCase). - **Envelope structure matches** (`data` / `page` / `links` / `actions`, `PageInfo` fields). - **GUID-as-String / DateTime round-trip** is consistent with how ASP.NET Core serializes `Guid` and `DateTime`. **Repository implementation** (`doujin_api_repository.dart`): - Correct Dio usage; no string-concatenated query strings (`queryParameters` map is used). No path-injection risk: `getDoujin` builds `/api/doujins/$id` and the backend route constraint `/{id:guid}` validates format at routing time (malformed id → 404). - Defensive `PageInfo` fallback when the envelope omits `page` is harmless — the backend always sets it (verified: every `CollectionResponse` construction across all 6 endpoint files passes a non-null `PageInfo`). **Static security scan:** clean — no hardcoded secrets, shell injection, `eval`/`exec`, pickle, SQL injection, or insecure-transport patterns. ### Minor non-blocking notes 1. **`listDoujins` / `search` totalItems fallback is imprecise** (`doujin_api_repository.dart:20`, `:69`). When `envelope.page` is null, the synthesized `PageInfo` uses `totalItems: envelope.data.length`. This is the current page size, not the true total. Since the backend always populates `page`, this never triggers in practice — but if it ever did, the UI would show "page 1 of 1" for a multi-page result. Low risk; consider documenting as a fallback-only path or logging when it occurs. 2. **`doujin_models.g.dart` / `envelope.g.dart` coverage is low** (50.8% / 50.7% per CI). This is expected for generated `fromJson`/`toJson` code (the `_$XFromJson` constructors have many branches for nullable/defaulted fields). Not actionable, just noting it explains the 72.2% total. 3. **`SearchQuery.toJson()` includes null fields** (`search_query.dart`). The test `models_test.dart` asserts `json.containsKey('excludeTags')` is true for unset fields. This is harmless (backend ignores nulls) and arguably more debuggable, but it diverges from a "sparse PATCH" style. The backend `SearchDoujinsDto` treats null filters as "no filter" either way, so no correctness impact. No security issues. No logic errors. No blocking findings. ### Verification **CI/CD:** Flutter coverage comment #491 is current for head `4186c537` (72.2%, 455/630 lines) and was generated at 23:57:34Z — matching the PR head. The `flutter-ci.yml` job posts coverage only on a successful `flutter analyze` + `flutter test`, which the PR body confirms (56/56 passed: 34 existing + 22 new). Local build/test skipped per CI-evidence policy. *Note: backend coverage comment was not posted for this PR (head branch is Flutter-only changes), which is expected — the C# backend is unchanged here.* --- *Automated daily review. I never merge PRs. This is a PR conversation comment with file references; it is not a formal Forgejo review-approval state (the current MCP integration cannot create those).*
Author
Member

Thanks for the thorough review — the wire-format verification is much appreciated.

All 3 minor notes are acknowledged as non-actionable:

  1. totalItems fallback — documented as defensive only; backend always populates page. Will add a debugPrint if the fallback ever triggers.
  2. Generated code coverage — expected for fromJson/toJson branches; will exclude *.g.dart from coverage reporting in Phase 10f (the analysis_options.yaml already excludes them from analysis).
  3. SearchQuery null fields — intentional for debuggability; backend treats null as "no filter" so no correctness impact.

No blocking findings — ready for merge.

Thanks for the thorough review — the wire-format verification is much appreciated. All 3 minor notes are acknowledged as non-actionable: 1. **totalItems fallback** — documented as defensive only; backend always populates `page`. Will add a `debugPrint` if the fallback ever triggers. 2. **Generated code coverage** — expected for `fromJson`/`toJson` branches; will exclude `*.g.dart` from coverage reporting in Phase 10f (the `analysis_options.yaml` already excludes them from analysis). 3. **SearchQuery null fields** — intentional for debuggability; backend treats null as "no filter" so no correctness impact. No blocking findings — ready for merge.
bjoern merged commit 7cf332b831 into main 2026-06-30 00:05:47 +02:00
bjoern deleted branch feat/flutter-data-models 2026-06-30 00:05:47 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!31
No description provided.