Skip to content

Commit f5bab8c

Browse files
dmealingclaude
andcommitted
fix(integration-tests): Java TPH harness coerces the enum-typed @Discriminator
release-gate (java) was failing (uncovered once the Maven dep-resolution fix let it actually run): TphGeneratedApiContractConformanceTest's generated sources failed to compile with "String cannot be converted to AuthDto.AuthType". Proven pre-existing (reproduces with the pre-FR-019 generator) — the TPH api-contract fixture's `type` discriminator is a field.enum, so the generated AuthDto types it as the AuthType enum (the cross-port typed-enum contract), but the Java test scaffolding still assumed a String-typed discriminator. Kotlin's release-gate passed because its harness seeds via HTTP POST (discriminator from the URL) and never constructs a DTO with the raw String. Bring the Java harness/scaffolding in line with the enum-typed discriminator: - GeneratedTphControllerHarness: resolve the generated nested AuthType reflectively, bind the ctor with it, and coerce the seed's String `type` via Enum.valueOf. - InMemoryAuthRepositorySource: coerce the URL discriminator with AuthType.valueOf on create, and compare via a.type().name() (String-vs-enum equality was silently always-false, which would have broken per-subtype scoping). TphGeneratedApiContractConformanceTest now 5/0 locally (in-process MockMvc, no docker). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent c2800bd commit f5bab8c

2 files changed

Lines changed: 18 additions & 8 deletions

File tree

server/java/integration-tests/src/test/java/com/metaobjects/integration/api/tph/generated/GeneratedTphControllerHarness.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ public final class GeneratedTphControllerHarness implements AutoCloseable {
7272

7373
private final ObjectMapper mapper = new ObjectMapper();
7474
private final URLClassLoader classLoader;
75-
private final Constructor<?> dtoCtor; // (Long id, String type, String reference, Integer quantity, BigDecimal copayAmount, String approver)
75+
private final Constructor<?> dtoCtor; // (Long id, AuthType type, String reference, Integer quantity, BigDecimal copayAmount, String approver)
76+
private final Class<?> authTypeClass; // the generated value-constrained enum discriminator (AuthDto.AuthType)
7677
private final Constructor<?> controllerCtor; // (AuthRepository)
7778
private final Constructor<?> repoCtor; // (List<AuthDto> seed)
7879
private final List<Map<String, Object>> seedRows;
@@ -109,8 +110,11 @@ public GeneratedTphControllerHarness(Path corpusRoot, Path genDir, List<Map<Stri
109110
// 5. Load the compiled classes (child of the test loader so Spring + runtime types resolve).
110111
this.classLoader = new URLClassLoader(new URL[]{ classesDir.toUri().toURL() }, getClass().getClassLoader());
111112
Class<?> dtoClass = classLoader.loadClass(DTO_FQCN);
113+
// `type` is the value-constrained enum discriminator (field.enum): the generated DTO ctor
114+
// takes the nested AuthType, not String. Resolve the enum reflectively to build seed rows.
115+
this.authTypeClass = classLoader.loadClass(DTO_FQCN + "$AuthType");
112116
this.dtoCtor = dtoClass.getDeclaredConstructor(
113-
Long.class, String.class, String.class, Integer.class, BigDecimal.class, String.class);
117+
Long.class, authTypeClass, String.class, Integer.class, BigDecimal.class, String.class);
114118
Class<?> repoInterface = classLoader.loadClass(REPO_FQCN);
115119
this.controllerCtor = classLoader.loadClass(CONTROLLER_FQCN).getDeclaredConstructor(repoInterface);
116120
this.repoCtor = classLoader.loadClass(InMemoryAuthRepositorySource.FQCN).getDeclaredConstructor(List.class);
@@ -162,7 +166,10 @@ private Object dtoFromRow(Map<String, Object> row) throws Exception {
162166
Object copayRaw = row.get("copayAmount");
163167
BigDecimal copayAmount = copayRaw == null ? null : new BigDecimal(String.valueOf(copayRaw));
164168
String approver = (String) row.get("approver");
165-
return dtoCtor.newInstance(id, type, reference, quantity, copayAmount, approver);
169+
// Coerce the seed's String discriminator into the generated AuthType enum value.
170+
@SuppressWarnings({ "unchecked", "rawtypes" })
171+
Object typeEnum = type == null ? null : Enum.valueOf((Class) authTypeClass, type);
172+
return dtoCtor.newInstance(id, typeEnum, reference, quantity, copayAmount, approver);
166173
}
167174

168175
private static MetaDataLoader loadCorpus(Path metaJson) {

server/java/integration-tests/src/test/java/com/metaobjects/integration/api/tph/generated/InMemoryAuthRepositorySource.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,22 +85,25 @@ public Optional<AuthDto> findById(Long id) {
8585
@Override
8686
public List<AuthDto> listByType(String discriminator, int limit, int offset, SortClause sort, List<FilterPredicate> filters) {
8787
List<AuthDto> scoped = new ArrayList<>();
88-
for (AuthDto a : rows) if (discriminator.equals(a.type())) scoped.add(a);
88+
for (AuthDto a : rows) if (discriminator.equals(a.type().name())) scoped.add(a);
8989
return page(sorted(filtered(scoped, filters), sort), limit, offset);
9090
}
9191
9292
@Override
9393
public Optional<AuthDto> findByIdAndType(Long id, String discriminator) {
9494
for (AuthDto a : rows)
95-
if (id.equals(a.id()) && discriminator.equals(a.type())) return Optional.of(a);
95+
if (id.equals(a.id()) && discriminator.equals(a.type().name())) return Optional.of(a);
9696
return Optional.empty();
9797
}
9898
9999
@Override
100100
public AuthDto createWithType(String discriminator, AuthDto dto) {
101101
// Discriminator injected from the URL — dto.type() is intentionally ignored.
102+
// `type` is a value-constrained enum (field.enum discriminator), so coerce the
103+
// String URL discriminator into the generated AuthType (its members ARE the
104+
// discriminator values).
102105
AuthDto saved = new AuthDto(
103-
nextId.getAndIncrement(), discriminator, dto.reference(),
106+
nextId.getAndIncrement(), AuthDto.AuthType.valueOf(discriminator), dto.reference(),
104107
dto.quantity(), dto.copayAmount(), dto.approver());
105108
rows.add(saved);
106109
return saved;
@@ -110,7 +113,7 @@ public AuthDto createWithType(String discriminator, AuthDto dto) {
110113
public Optional<AuthDto> updateByIdAndType(Long id, String discriminator, AuthDto dto) {
111114
for (int i = 0; i < rows.size(); i++) {
112115
AuthDto cur = rows.get(i);
113-
if (!id.equals(cur.id()) || !discriminator.equals(cur.type())) continue;
116+
if (!id.equals(cur.id()) || !discriminator.equals(cur.type().name())) continue;
114117
// Partial patch; discriminator (type) and id are immutable.
115118
AuthDto merged = new AuthDto(
116119
id, cur.type(),
@@ -126,7 +129,7 @@ public Optional<AuthDto> updateByIdAndType(Long id, String discriminator, AuthDt
126129
127130
@Override
128131
public boolean deleteByIdAndType(Long id, String discriminator) {
129-
return rows.removeIf(a -> id.equals(a.id()) && discriminator.equals(a.type()));
132+
return rows.removeIf(a -> id.equals(a.id()) && discriminator.equals(a.type().name()));
130133
}
131134
132135
// --- helpers ---

0 commit comments

Comments
 (0)