|
| 1 | +package com.metaobjects.loader.parser.json; |
| 2 | + |
| 3 | +import com.metaobjects.MetaDataException; |
| 4 | +import com.metaobjects.loader.InMemoryStringSource; |
| 5 | +import com.metaobjects.loader.MetaDataLoader; |
| 6 | +import com.metaobjects.registry.SharedRegistryTestBase; |
| 7 | +import org.junit.Test; |
| 8 | + |
| 9 | +import java.util.Collections; |
| 10 | +import java.util.List; |
| 11 | + |
| 12 | +import static org.junit.Assert.*; |
| 13 | + |
| 14 | +/** |
| 15 | + * FR-033 (sub-step B2a) — structural-child placement enforcement at strict load. |
| 16 | + * |
| 17 | + * <p>The strict per-subtype constraint model (read from {@code spec/metamodel/*.json}) |
| 18 | + * replaced Java's broad child rules with the same strict child graph TS enforces. A |
| 19 | + * STRUCTURAL child (field/identity/source/validator/relationship/… — NOT an attr) placed |
| 20 | + * under a parent whose registered child rules do NOT admit it must be rejected at load — |
| 21 | + * the structural analogue of {@code ERR_UNKNOWN_ATTR} for misplaced attrs.</p> |
| 22 | + * |
| 23 | + * <p>This is the Java mirror of the TS gating test |
| 24 | + * {@code server/typescript/packages/metadata/test/child-placement-enforcement.test.ts}. |
| 25 | + * Java enforces placement eagerly at {@code MetaData.addChild} via |
| 26 | + * {@code MetaDataRegistry.acceptsChild} (which consults the strict child graph), throwing |
| 27 | + * an {@link com.metaobjects.InvalidMetaDataException} (a {@link MetaDataException}) during |
| 28 | + * the parse. The thrown error carries {@link com.metaobjects.ErrorCode#ERR_CHILD_NOT_ALLOWED} |
| 29 | + * and a detail naming the parent, the rejected child ({@code type.subType 'name'}), and |
| 30 | + * the supported children — matching the cross-port {@code ERR_CHILD_NOT_ALLOWED} contract |
| 31 | + * in {@code fixtures/conformance/ERROR-CODES.json}.</p> |
| 32 | + * |
| 33 | + * <p>Two genuinely-misplaced cases are pinned:</p> |
| 34 | + * <ol> |
| 35 | + * <li>A {@code relationship.*} child under an {@code object.projection} — the strict |
| 36 | + * projection child set is field/identity/validator/layout/source, with NO |
| 37 | + * relationship (a projection expresses derivation via {@code @via}, never a |
| 38 | + * relationship child; see ADR-0028 / {@code spec/metamodel/object.json}).</li> |
| 39 | + * <li>A structural child (a {@code field}) under an attr-only type |
| 40 | + * ({@code validator.required}, whose strict child set is empty).</li> |
| 41 | + * </ol> |
| 42 | + */ |
| 43 | +public class StrictChildPlacementTest extends SharedRegistryTestBase { |
| 44 | + |
| 45 | + private MetaDataLoader newTestLoader() { |
| 46 | + return createTestLoader("StrictChildPlacement", Collections.emptyList()); |
| 47 | + } |
| 48 | + |
| 49 | + /** Attempt a strict load of canonical JSON; returns the thrown exception (or null). */ |
| 50 | + private MetaDataException loadExpectingThrow(String canonical, String id) { |
| 51 | + MetaDataLoader loader = newTestLoader(); |
| 52 | + try { |
| 53 | + loader.load(List.of(new InMemoryStringSource(canonical, id))); |
| 54 | + return null; |
| 55 | + } catch (MetaDataException e) { |
| 56 | + return e; |
| 57 | + } |
| 58 | + } |
| 59 | + |
| 60 | + // ----------------------------------------------------------------------- |
| 61 | + // 1 — A relationship child under object.projection is NOT admitted by the |
| 62 | + // strict projection child graph → rejected at load. |
| 63 | + // ----------------------------------------------------------------------- |
| 64 | + |
| 65 | + @Test |
| 66 | + public void relationshipUnderProjectionIsRejected() { |
| 67 | + // A projection's strict children omit relationship.* entirely. |
| 68 | + String canonical = |
| 69 | + "{ \"metadata.root\": { \"package\": \"acme\", \"children\": [" |
| 70 | + + " { \"object.entity\": { \"name\": \"Order\", \"children\": [" |
| 71 | + + " { \"field.long\": { \"name\": \"id\" } }," |
| 72 | + + " { \"identity.primary\": { \"name\": \"pk\", \"@fields\": \"id\" } }" |
| 73 | + + " ] } }," |
| 74 | + + " { \"object.projection\": { \"name\": \"OrderView\", \"extends\": \"Order\"," |
| 75 | + + " \"children\": [" |
| 76 | + + " { \"relationship.association\": { \"name\": \"bogus\"," |
| 77 | + + " \"@cardinality\": \"one\", \"@objectRef\": \"Order\" } }" |
| 78 | + + " ] } }" |
| 79 | + + "] } }"; |
| 80 | + |
| 81 | + MetaDataException e = loadExpectingThrow(canonical, "rel-under-projection.json"); |
| 82 | + assertNotNull("a relationship child under object.projection must be REJECTED at " |
| 83 | + + "strict load (projection's strict child set has no relationship)", e); |
| 84 | + String msg = e.getMessage(); |
| 85 | + assertTrue("error must name the misplaced relationship child; got: " + msg, |
| 86 | + msg != null && msg.contains("relationship")); |
| 87 | + assertTrue("error must name the projection parent; got: " + msg, |
| 88 | + msg != null && msg.contains("projection")); |
| 89 | + } |
| 90 | + |
| 91 | + // ----------------------------------------------------------------------- |
| 92 | + // 2 — A structural (field) child under an attr-only type (validator.required, |
| 93 | + // strict child set = []) → rejected at load. |
| 94 | + // ----------------------------------------------------------------------- |
| 95 | + |
| 96 | + @Test |
| 97 | + public void fieldUnderValidatorIsRejected() { |
| 98 | + // validator.required admits no structural children — a field child is misplaced. |
| 99 | + String canonical = |
| 100 | + "{ \"metadata.root\": { \"package\": \"acme\", \"children\": [" |
| 101 | + + " { \"object.entity\": { \"name\": \"Widget\", \"children\": [" |
| 102 | + + " { \"field.long\": { \"name\": \"id\" } }," |
| 103 | + + " { \"field.string\": { \"name\": \"code\", \"children\": [" |
| 104 | + + " { \"validator.required\": { \"name\": \"req\", \"children\": [" |
| 105 | + + " { \"field.string\": { \"name\": \"bogus\" } }" |
| 106 | + + " ] } }" |
| 107 | + + " ] } }," |
| 108 | + + " { \"identity.primary\": { \"name\": \"pk\", \"@fields\": \"id\" } }" |
| 109 | + + " ] } }" |
| 110 | + + "] } }"; |
| 111 | + |
| 112 | + MetaDataException e = loadExpectingThrow(canonical, "field-under-validator.json"); |
| 113 | + assertNotNull("a field child under an attr-only validator.required must be REJECTED " |
| 114 | + + "at strict load (validator's strict child set is empty)", e); |
| 115 | + String msg = e.getMessage(); |
| 116 | + assertTrue("error must name the misplaced field child; got: " + msg, |
| 117 | + msg != null && msg.contains("field")); |
| 118 | + assertTrue("error must name the validator parent; got: " + msg, |
| 119 | + msg != null && msg.contains("validator")); |
| 120 | + } |
| 121 | +} |
0 commit comments