Skip to content

Commit a019e46

Browse files
dmealingclaude
andcommitted
feat(java/metadata): identity.reference type (Java parity)
Adds ReferenceIdentity as a third concrete subtype of MetaIdentity, alongside PrimaryIdentity and SecondaryIdentity. Mirrors the TypeScript @metaobjects/metadata design (identity-anchored references + @Enforce attr). Attributes: - @fields — column(s) on this entity that hold the reference value(s). - @references — target entity (bare or dotted Entity.field / Entity.fA,fB form). - @Enforce — optional boolean (default true). false → logical-only reference; codegen skips physical FK constraint. MetaIdentity gains SUBTYPE_REFERENCE, ATTR_REFERENCES + ATTR_ENFORCE constants, and an isReference() convenience method. IdentityTypesMetaDataProvider registers the new subtype. TypeScript MetaReferenceIdentity + Java ReferenceIdentity carry identical attr names + semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent cffc6ae commit a019e46

3 files changed

Lines changed: 149 additions & 2 deletions

File tree

java/metadata/src/main/java/com/metaobjects/identity/IdentityTypesMetaDataProvider.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55

66
/**
77
* Identity Types MetaData provider.
8-
* Registers distinct identity types (PrimaryIdentity, SecondaryIdentity) for object identification.
8+
* Registers distinct identity types (PrimaryIdentity, SecondaryIdentity, ReferenceIdentity)
9+
* for object identification and cross-entity reference declaration.
910
* Depends on core-types for metadata.base inheritance.
1011
*/
1112
public class IdentityTypesMetaDataProvider implements MetaDataTypeProvider {
@@ -15,6 +16,7 @@ public void registerTypes(MetaDataRegistry registry) {
1516
// Register distinct identity types
1617
PrimaryIdentity.registerTypes(registry);
1718
SecondaryIdentity.registerTypes(registry);
19+
ReferenceIdentity.registerTypes(registry);
1820
}
1921

2022
@Override
@@ -29,6 +31,6 @@ public String[] getDependencies() {
2931

3032
@Override
3133
public String getDescription() {
32-
return "Identity Types (primary, secondary) for object identification";
34+
return "Identity Types (primary, secondary, reference) for object identification and cross-entity references";
3335
}
3436
}

java/metadata/src/main/java/com/metaobjects/identity/MetaIdentity.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,22 @@ public abstract class MetaIdentity extends MetaData {
3939
/** Secondary key subtype - business identifiers, multiple allowed */
4040
public final static String SUBTYPE_SECONDARY = "secondary";
4141

42+
/** Reference subtype - this entity has fields whose value(s) identify an instance of another entity. */
43+
public final static String SUBTYPE_REFERENCE = "reference";
44+
4245
// === ESSENTIAL ATTRIBUTES ===
4346
/** Array of field names that comprise this identity */
4447
public final static String ATTR_FIELDS = "fields";
4548

4649
/** Generation strategy for identity values */
4750
public final static String ATTR_GENERATION = "generation";
4851

52+
/** Reference identity: target entity (bare or dotted Entity.field / Entity.fA,fB form). */
53+
public final static String ATTR_REFERENCES = "references";
54+
55+
/** Reference identity: physical-enforcement flag. Default true (hard FK constraint). */
56+
public final static String ATTR_ENFORCE = "enforce";
57+
4958
// === GENERATION STRATEGY CONSTANTS ===
5059
/** Auto-incrementing integer (database chooses implementation) */
5160
public static final String GENERATION_INCREMENT = "increment";
@@ -148,6 +157,10 @@ public boolean isSecondary() {
148157
return SUBTYPE_SECONDARY.equals(getSubType());
149158
}
150159

160+
public boolean isReference() {
161+
return SUBTYPE_REFERENCE.equals(getSubType());
162+
}
163+
151164
public boolean hasGeneration() {
152165
return getGeneration() != null;
153166
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.metaobjects.identity;
2+
3+
import com.metaobjects.MetaData;
4+
import com.metaobjects.attr.BooleanAttribute;
5+
import com.metaobjects.attr.MetaAttribute;
6+
import com.metaobjects.attr.StringAttribute;
7+
import com.metaobjects.registry.MetaDataRegistry;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
import java.util.ArrayList;
12+
import java.util.Collections;
13+
import java.util.List;
14+
15+
import static com.metaobjects.object.MetaObject.ATTR_DESCRIPTION;
16+
17+
/**
18+
* Reference identity — declares that this entity has fields whose value(s)
19+
* identify an instance of another entity.
20+
*
21+
* Paradigm-neutral: maps to SQL foreign key, document linked reference,
22+
* graph edge target, or OO pointer/reference. The metamodel itself does
23+
* not commit to a backend implementation.
24+
*
25+
* Attributes:
26+
* - {@code @fields} — column(s) on THIS entity that hold the reference value(s).
27+
* - {@code @references} — target entity. Bare name (e.g. "Program") resolves to
28+
* the target's primary identity. Dotted forms ("Program.id" or
29+
* "Program.fieldA,fieldB") target an explicit field set on that entity.
30+
* - {@code @enforce} — optional boolean (default true). When true the backend
31+
* physically enforces the reference (SQL FK constraint, document validation
32+
* rule, graph edge guarantee). Set false for navigation/typing/codegen-only
33+
* references that may dangle at the backend level.
34+
*
35+
* Parallel to TypeScript's {@code MetaReferenceIdentity} in
36+
* {@code @metaobjects/metadata}.
37+
*/
38+
public class ReferenceIdentity extends MetaIdentity {
39+
40+
private static final Logger log = LoggerFactory.getLogger(ReferenceIdentity.class);
41+
42+
/**
43+
* Create a reference identity with the specified name.
44+
* The subType is automatically set to "reference".
45+
*/
46+
public ReferenceIdentity(String name) {
47+
super(SUBTYPE_REFERENCE, name);
48+
}
49+
50+
/**
51+
* Register ReferenceIdentity type with the MetaDataRegistry.
52+
* Called by IdentityTypesMetaDataProvider during service discovery.
53+
*/
54+
public static void registerTypes(MetaDataRegistry registry) {
55+
registry.registerType(ReferenceIdentity.class, def -> {
56+
def.type(TYPE_IDENTITY).subType(SUBTYPE_REFERENCE)
57+
.description("Reference identity — fields on this entity that identify an instance of another entity")
58+
.inheritsFrom(MetaData.TYPE_METADATA, MetaData.SUBTYPE_BASE);
59+
60+
def.optionalAttributeWithConstraints(ATTR_FIELDS).ofType(StringAttribute.SUBTYPE_STRING).asArray();
61+
def.optionalAttributeWithConstraints(ATTR_REFERENCES).ofType(StringAttribute.SUBTYPE_STRING).asSingle();
62+
def.optionalAttributeWithConstraints(ATTR_ENFORCE).ofType(BooleanAttribute.SUBTYPE_BOOLEAN).asSingle();
63+
def.optionalAttributeWithConstraints(ATTR_DESCRIPTION).ofType(StringAttribute.SUBTYPE_STRING).asSingle();
64+
65+
// ACCEPTS ANY ATTRIBUTES (for extensibility from service providers)
66+
def.optionalChild(MetaAttribute.TYPE_ATTR, "*", "*");
67+
});
68+
}
69+
70+
/**
71+
* Raw {@code @references} attr value, unparsed. Null when the attr is absent.
72+
*/
73+
public String getReferencesRaw() {
74+
return hasMetaAttr(ATTR_REFERENCES) ?
75+
getMetaAttr(ATTR_REFERENCES).getValueAsString() : null;
76+
}
77+
78+
/**
79+
* Target entity name — the segment before the first dot in {@code @references},
80+
* or the whole value when bare. Null if the attr is absent.
81+
*/
82+
public String getTargetEntity() {
83+
String raw = getReferencesRaw();
84+
if (raw == null) return null;
85+
int dot = raw.indexOf('.');
86+
return dot == -1 ? raw : raw.substring(0, dot);
87+
}
88+
89+
/**
90+
* Target field names parsed from the dotted form. Empty list when {@code @references}
91+
* is bare (meaning "use the target entity's primary identity"). Multi-element when
92+
* the dotted form lists comma-separated fields (compound FK).
93+
*/
94+
public List<String> getTargetFields() {
95+
String raw = getReferencesRaw();
96+
if (raw == null) return Collections.emptyList();
97+
int dot = raw.indexOf('.');
98+
if (dot == -1) return Collections.emptyList();
99+
String suffix = raw.substring(dot + 1);
100+
List<String> out = new ArrayList<>();
101+
for (String part : suffix.split(",")) {
102+
String trimmed = part.trim();
103+
if (!trimmed.isEmpty()) out.add(trimmed);
104+
}
105+
return out;
106+
}
107+
108+
/**
109+
* Whether the reference is physically enforced by the backend.
110+
* Default true (hard FK constraint emitted). Explicit {@code @enforce: false}
111+
* marks the reference as logical-only — codegen skips physical constraints.
112+
*/
113+
public boolean isEnforced() {
114+
if (!hasMetaAttr(ATTR_ENFORCE)) return true;
115+
MetaAttribute attr = getMetaAttr(ATTR_ENFORCE);
116+
Object value = attr.getValue();
117+
if (value instanceof Boolean) return (Boolean) value;
118+
// String fallback for value coercion edge cases — explicit "false" means soft.
119+
return !"false".equalsIgnoreCase(attr.getValueAsString());
120+
}
121+
122+
@Override
123+
public String toString() {
124+
return String.format("%s[%s:%s]{%s -> %s}%s",
125+
getClass().getSimpleName(),
126+
getType(),
127+
getSubType(),
128+
getName(),
129+
getReferencesRaw(),
130+
isEnforced() ? "" : " [SOFT]");
131+
}
132+
}

0 commit comments

Comments
 (0)