TemporalFieldTraits<T>: replace hard-coded entity_id/valid_from/valid_until with a trait #10

Closed
opened 2026-04-29 14:12:52 +02:00 by uwe.admin · 5 comments
Owner

Extend TemporalRepository<T> (and any other decorator that touches canonical temporal fields) to access fields via a TemporalFieldTraits<TDto> specialisation instead of hard-coded member names like dto->valid_from, dto->valid_until, dto->entity_id.

Motivation and design sketch are in the first comment.

Acceptance

  • include/oatpp-authkit/repo/TemporalFieldTraits.hpp defines an undefined primary template TemporalFieldTraits<TDto> exposing static accessors entityId(d), validFrom(d), validUntil(d) returning oatpp::String&.
  • A registration macro at namespace scope (e.g. OATPP_AUTHKIT_REGISTER_TEMPORAL(PersonDto, entity_id, valid_from, valid_until)) generates the specialisation in one line.
  • TemporalRepository<T> rewrites every direct field access through the trait. static_assert verifies the specialisation exists in the constructor.
  • ITemporalEntity becomes redundant — either remove it or downgrade it to a tag with no required fields. (Decision deferred to implementation time.)
  • Existing tests still pass; one new test registers a DTO whose temporal columns are not named valid_from/valid_until and exercises the full save → close → history path against it.

Out of scope

  • Generalising beyond temporal fields (e.g. soft-delete columns, audit columns) — separate issue if it ever matters.
  • A reflection-style "discover all fields" mechanism — explicit registration is fine, the macro hides the boilerplate.

Notes

Not urgent — Person pilot in fewo-webapp#457 ships fine without this. Tackle when there's a free slot or when a second temporal DTO arrives with column names that don't match the canonical three.

Extend `TemporalRepository<T>` (and any other decorator that touches canonical temporal fields) to access fields via a `TemporalFieldTraits<TDto>` specialisation instead of hard-coded member names like `dto->valid_from`, `dto->valid_until`, `dto->entity_id`. Motivation and design sketch are in the first comment. ## Acceptance - `include/oatpp-authkit/repo/TemporalFieldTraits.hpp` defines an undefined primary template `TemporalFieldTraits<TDto>` exposing static accessors `entityId(d)`, `validFrom(d)`, `validUntil(d)` returning `oatpp::String&`. - A registration macro at namespace scope (e.g. `OATPP_AUTHKIT_REGISTER_TEMPORAL(PersonDto, entity_id, valid_from, valid_until)`) generates the specialisation in one line. - `TemporalRepository<T>` rewrites every direct field access through the trait. `static_assert` verifies the specialisation exists in the constructor. - `ITemporalEntity` becomes redundant — either remove it or downgrade it to a tag with no required fields. (Decision deferred to implementation time.) - Existing tests still pass; one new test registers a DTO whose temporal columns are *not* named `valid_from`/`valid_until` and exercises the full save → close → history path against it. ## Out of scope - Generalising beyond temporal fields (e.g. soft-delete columns, audit columns) — separate issue if it ever matters. - A reflection-style "discover all fields" mechanism — explicit registration is fine, the macro hides the boilerplate. ## Notes Not urgent — Person pilot in fewo-webapp#457 ships fine without this. Tackle when there's a free slot or when a second temporal DTO arrives with column names that don't match the canonical three.
Author
Owner

C++ traits in one paragraph

A trait is a class template you specialise per type to give that type extra compile-time information or behaviour without touching the type itself. The primary template is usually undefined or empty; users provide a template<> struct Trait<MyType> { ... } specialisation that exposes static members (types, constants, or functions). The library code then writes Trait<T>::something instead of hard-coding assumptions about T. It is the C++17/20 way to do "structural typing" — like Rust traits or Go interfaces, but resolved at compile time with zero runtime cost.

Where #7/#8 hard-codes things today

TemporalRepository<T> is generic on TDto but actually only works with DTOs that have three specific oatpp String fields named entity_id, valid_from, valid_until. The header even says so:

The marker is asserted at compile time; the field shape is documentation-enforced (oatpp DTOs do not expose a static field-list mechanism the decorator could verify).

So live->valid_until = oatpp::String(nowIso) works only because every consumer DTO happens to use that exact name. Rename the column in one DTO and it silently fails to compile (or worse, the wrong field gets touched if names overlap).

What a trait-based extension looks like

Add a TemporalFieldTraits<T> next to ITemporalEntity:

template <class TDto>
struct TemporalFieldTraits;  // primary: undefined → hard error if unspecialised

// User opts in once per DTO, near the DTO definition:
template<> struct TemporalFieldTraits<PersonDto> {
    static oatpp::String& entityId   (PersonDto& d) { return d.entity_id;   }
    static oatpp::String& validFrom  (PersonDto& d) { return d.valid_from;  }
    static oatpp::String& validUntil (PersonDto& d) { return d.valid_until; }
};

Then TemporalRepository<T> stops touching ->valid_until directly:

using F = TemporalFieldTraits<TDto>;
F::validUntil(*live) = oatpp::String(nowIso);
m_inner->save(live);

A static_assert(std::is_same_v<decltype(F::validFrom(d)), oatpp::String&>) in the constructor turns "field shape mismatch" from a runtime mystery into a compile error pointing at the DTO.

Why it is worth doing

  • Decorator stops coupling to specific field names.
  • DTOs that already have valid_from_ts or effective_from can opt in without renaming columns.
  • The static_assert replaces the docstring with an enforcement.
  • ITemporalEntity marker becomes redundant (the trait is the marker).
## C++ traits in one paragraph A **trait** is a class template you specialise per type to give that type extra compile-time information or behaviour without touching the type itself. The primary template is usually undefined or empty; users provide a `template<> struct Trait<MyType> { ... }` specialisation that exposes static members (types, constants, or functions). The library code then writes `Trait<T>::something` instead of hard-coding assumptions about `T`. It is the C++17/20 way to do "structural typing" — like Rust traits or Go interfaces, but resolved at compile time with zero runtime cost. ## Where #7/#8 hard-codes things today `TemporalRepository<T>` is generic on `TDto` but actually only works with DTOs that have three specific oatpp `String` fields named `entity_id`, `valid_from`, `valid_until`. The header even says so: > The marker is asserted at compile time; the field shape is documentation-enforced (oatpp DTOs do not expose a static field-list mechanism the decorator could verify). So `live->valid_until = oatpp::String(nowIso)` works only because every consumer DTO happens to use that exact name. Rename the column in one DTO and it silently fails to compile (or worse, the wrong field gets touched if names overlap). ## What a trait-based extension looks like Add a `TemporalFieldTraits<T>` next to `ITemporalEntity`: ```cpp template <class TDto> struct TemporalFieldTraits; // primary: undefined → hard error if unspecialised // User opts in once per DTO, near the DTO definition: template<> struct TemporalFieldTraits<PersonDto> { static oatpp::String& entityId (PersonDto& d) { return d.entity_id; } static oatpp::String& validFrom (PersonDto& d) { return d.valid_from; } static oatpp::String& validUntil (PersonDto& d) { return d.valid_until; } }; ``` Then `TemporalRepository<T>` stops touching `->valid_until` directly: ```cpp using F = TemporalFieldTraits<TDto>; F::validUntil(*live) = oatpp::String(nowIso); m_inner->save(live); ``` A `static_assert(std::is_same_v<decltype(F::validFrom(d)), oatpp::String&>)` in the constructor turns "field shape mismatch" from a runtime mystery into a compile error pointing at the DTO. ## Why it is worth doing - Decorator stops coupling to specific field names. - DTOs that already have `valid_from_ts` or `effective_from` can opt in without renaming columns. - The static_assert replaces the docstring with an enforcement. - `ITemporalEntity` marker becomes redundant (the trait *is* the marker).
Author
Owner

Agent Evaluation

Feasibility: High. Single header file plus a one-line registration macro; rewrite a handful of dto->valid_* accesses in TemporalRepository.hpp to go through TemporalFieldTraits<T>::*. No external dependencies, no API break beyond requiring the macro at consumer sites.

Impact: Modest now (one consumer — the upcoming Person pilot — happens to use the canonical names anyway), real later. Removes the documentation-enforced field-shape constraint and turns shape mismatches into pinpoint compile errors. Also retires ITemporalEntity as a marker, since the trait specialisation is the marker.

Effort: Small. Estimate ~150 LOC across TemporalFieldTraits.hpp + edits to TemporalRepository.hpp + one new test that exercises a DTO with non-canonical column names.

Recommendation: Accept. The system is pre-production, no migration cost; better to land the trait surface before more consumers bake in the assumption.

Implementation plan

  1. New header include/oatpp-authkit/repo/TemporalFieldTraits.hpp: undefined primary TemporalFieldTraits<TDto>, registration macro OATPP_AUTHKIT_REGISTER_TEMPORAL(Dto, IdMember, FromMember, UntilMember) expanding to a specialisation with three static oatpp::String& accessors.
  2. Edit TemporalRepository.hpp: add using F = TemporalFieldTraits<TDto>;, replace every dto->entity_id / ->valid_from / ->valid_until access with F::entityId(*dto) etc. Add static_assert(std::is_same_v<decltype(F::validFrom(*dto)), oatpp::String&>) in the constructor.
  3. Decide ITemporalEntity fate — recommend deleting it; the trait specialisation now carries the contract. Remove the static_assert(std::is_base_of_v<ITemporalEntity, TDto>).
  4. Update existing test DTOs to register via the macro.
  5. New test test_temporal_field_traits.cpp: a DTO with effective_from / effective_until (non-canonical names), full save → close → history flow.
  6. Bump library version to 0.5.0; consumers (fewo-webapp, when retagging) update GIT_TAG and add the macro to their temporal DTOs.
## Agent Evaluation **Feasibility:** High. Single header file plus a one-line registration macro; rewrite a handful of `dto->valid_*` accesses in `TemporalRepository.hpp` to go through `TemporalFieldTraits<T>::*`. No external dependencies, no API break beyond requiring the macro at consumer sites. **Impact:** Modest now (one consumer — the upcoming Person pilot — happens to use the canonical names anyway), real later. Removes the documentation-enforced field-shape constraint and turns shape mismatches into pinpoint compile errors. Also retires `ITemporalEntity` as a marker, since the trait specialisation *is* the marker. **Effort:** Small. Estimate ~150 LOC across `TemporalFieldTraits.hpp` + edits to `TemporalRepository.hpp` + one new test that exercises a DTO with non-canonical column names. **Recommendation:** Accept. The system is pre-production, no migration cost; better to land the trait surface before more consumers bake in the assumption. ### Implementation plan 1. New header `include/oatpp-authkit/repo/TemporalFieldTraits.hpp`: undefined primary `TemporalFieldTraits<TDto>`, registration macro `OATPP_AUTHKIT_REGISTER_TEMPORAL(Dto, IdMember, FromMember, UntilMember)` expanding to a specialisation with three static `oatpp::String&` accessors. 2. Edit `TemporalRepository.hpp`: add `using F = TemporalFieldTraits<TDto>;`, replace every `dto->entity_id` / `->valid_from` / `->valid_until` access with `F::entityId(*dto)` etc. Add `static_assert(std::is_same_v<decltype(F::validFrom(*dto)), oatpp::String&>)` in the constructor. 3. Decide `ITemporalEntity` fate — recommend deleting it; the trait specialisation now carries the contract. Remove the `static_assert(std::is_base_of_v<ITemporalEntity, TDto>)`. 4. Update existing test DTOs to register via the macro. 5. New test `test_temporal_field_traits.cpp`: a DTO with `effective_from` / `effective_until` (non-canonical names), full save → close → history flow. 6. Bump library version to 0.5.0; consumers (fewo-webapp, when retagging) update `GIT_TAG` and add the macro to their temporal DTOs.
uwe.admin added the
evaluated
label 2026-04-29 14:14:13 +02:00
Author
Owner

Evaluated #10 (Small) — recommend Accept.

Evaluated #10 (Small) — recommend Accept.
u.schuster added the
accepted
label 2026-04-29 14:18:22 +02:00
Author
Owner

Implemented #10 in 1baff07 — 8/8 tests pass, including new test_temporal_field_traits exercising a DTO with id/effective_from/effective_until column names. Auto-closed via Closes #10. Library bumped 0.4.0 → 0.5.0.

Implemented #10 in 1baff07 — 8/8 tests pass, including new test_temporal_field_traits exercising a DTO with id/effective_from/effective_until column names. Auto-closed via `Closes #10`. Library bumped 0.4.0 → 0.5.0.
Author
Owner

Implemented #10 → commit 1baff07. New repo/TemporalFieldTraits.hpp with undefined primary template + OATPP_AUTHKIT_REGISTER_TEMPORAL macro; TemporalRepository<T> rewritten to access fields through the trait; ITemporalEntity removed; existing decorator/interface tests migrated; new test_temporal_field_traits.cpp exercises a DTO with id/effective_from/effective_until columns end-to-end. Version bumped to 0.5.0; all 8 ctest cases pass.

Implemented #10 → commit 1baff07. New `repo/TemporalFieldTraits.hpp` with undefined primary template + `OATPP_AUTHKIT_REGISTER_TEMPORAL` macro; `TemporalRepository<T>` rewritten to access fields through the trait; `ITemporalEntity` removed; existing decorator/interface tests migrated; new `test_temporal_field_traits.cpp` exercises a DTO with `id`/`effective_from`/`effective_until` columns end-to-end. Version bumped to 0.5.0; all 8 ctest cases pass.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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: uwe.admin/oatpp-authkit#10
No description provided.