From cb225e485856415532ce89942916651fc7746d22 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 26 Jun 2026 15:22:39 +0100 Subject: [PATCH 1/2] Add test case for a "key type designator". A type designator slot may also be the key (or even the identifier) slot if its class. Combined with dict inlining, this can be used to do something like this: items: FooItem: # ... FooItem attributes BarItem: # ... BarItem attributes BazItem: # ... BazItem attributes where FooItem, BarItem, and BazItem are all subclasses of Item (which is the declared range of the `items` slot), and the keys of the `items` dictionary act both as identifiers to refer to one particular item, _and_ as type designators to indicate the precise type of each item (as a side-effect, this ensures that `items` can only contain one object of each type). This is a pattern that could be useful for NGMF and/or NGFF extensions, and that we should support. This commit adds an explicit test case for it. The test currently _fails_, because the ObjectConverter does not expect to find the type designator as the key in a "inlined-as-dict" object. --- .../linkml/core/ObjectConverterTest.java | 24 +++++++++++++++ core/src/test/linkml/samples.yaml | 29 +++++++++++++++++++ ...iner-of-keyed-self-designated-objects.yaml | 6 ++++ 3 files changed, 59 insertions(+) create mode 100644 core/src/test/resources/core/samples/container-of-keyed-self-designated-objects.yaml diff --git a/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java b/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java index 12dc534..67a0ed3 100644 --- a/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java +++ b/core/src/test/java/org/incenp/linkml/core/ObjectConverterTest.java @@ -43,6 +43,7 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -56,17 +57,20 @@ import org.incenp.linkml.core.samples.base.ContainerOfIRIIdentifiableObjects; import org.incenp.linkml.core.samples.base.ContainerOfInlinedObjects; import org.incenp.linkml.core.samples.base.ContainerOfIntegerValues; +import org.incenp.linkml.core.samples.base.ContainerOfKeyedSelfDesignatedObjects; import org.incenp.linkml.core.samples.base.ContainerOfReferences; import org.incenp.linkml.core.samples.base.ContainerOfSelfDesignatedObjects; import org.incenp.linkml.core.samples.base.ContainerOfSimpleDicts; import org.incenp.linkml.core.samples.base.ContainerOfSimpleObjects; import org.incenp.linkml.core.samples.base.DerivedCurieSelfDesignatedClass; +import org.incenp.linkml.core.samples.base.DerivedKeyedSelfDesignatedClass; import org.incenp.linkml.core.samples.base.DerivedMultiSelfDesignatedClass; import org.incenp.linkml.core.samples.base.DerivedSelfDesignatedClass; import org.incenp.linkml.core.samples.base.DerivedURISelfDesignatedClass; import org.incenp.linkml.core.samples.base.ExtensibleSimpleClass; import org.incenp.linkml.core.samples.base.ExtraSimpleDict; import org.incenp.linkml.core.samples.base.IRISimpleIdentifiableClass; +import org.incenp.linkml.core.samples.base.KeyedSelfDesignatedClass; import org.incenp.linkml.core.samples.base.MultivaluedSimpleDict; import org.incenp.linkml.core.samples.base.SampleEnum; import org.incenp.linkml.core.samples.base.SecondDerivedSelfDesignatedClass; @@ -515,6 +519,26 @@ void testMultiLevelTypeDesignators() throws IOException { roundtrip(cosdo); } + @Test + void testKeyTypeDesignator() throws IOException { + ContainerOfKeyedSelfDesignatedObjects cksdo = parse("container-of-keyed-self-designated-objects.yaml", + ContainerOfKeyedSelfDesignatedObjects.class); + + HashMap d = new HashMap<>(); + for ( KeyedSelfDesignatedClass o : cksdo.getObjects() ) { + d.put(o.getType(), o); + } + KeyedSelfDesignatedClass ksdc = d.get("KeyedSelfDesignatedClass"); + Assertions.assertNotNull(ksdc); + Assertions.assertEquals("Alice", ksdc.getFrobnicator()); + + ksdc = d.get("DerivedKeyedSelfDesignatedClass"); + Assertions.assertNotNull(ksdc); + Assertions.assertEquals("Bob", ksdc.getFrobnicator()); + Assertions.assertInstanceOf(DerivedKeyedSelfDesignatedClass.class, ksdc); + Assertions.assertEquals(123, ((DerivedKeyedSelfDesignatedClass) ksdc).getLength()); + } + @Test void testReferenceToIRIIdentifiers() throws IOException, LinkMLRuntimeException { ctx.addPrefix("PFX", "https://example.org/"); diff --git a/core/src/test/linkml/samples.yaml b/core/src/test/linkml/samples.yaml index 36c80e9..4cc4b16 100644 --- a/core/src/test/linkml/samples.yaml +++ b/core/src/test/linkml/samples.yaml @@ -387,6 +387,35 @@ classes: range: Anything multivalued: true + KeyedSelfDesignatedClass: + description: >- + A class with a slot that is both a type designator and a key slot. + attributes: + type: + designates_type: true + key: true + frobnicator: + + DerivedKeyedSelfDesignatedClass: + description: >- + A class that derives from a class with a slot that is both a type + designator and a key slot. + is_a: KeyedSelfDesignatedClass + attributes: + length: + range: integer + + ContainerOfKeyedSelfDesignatedObjects: + description: >- + A class with a slot whose range is set to a class with a slot that is + both a key slot and and a type designator slot. + attributes: + objects: + range: KeyedSelfDesignatedClass + multivalued: true + inlined: true + inlined_as_list: false + enums: diff --git a/core/src/test/resources/core/samples/container-of-keyed-self-designated-objects.yaml b/core/src/test/resources/core/samples/container-of-keyed-self-designated-objects.yaml new file mode 100644 index 0000000..5e964fb --- /dev/null +++ b/core/src/test/resources/core/samples/container-of-keyed-self-designated-objects.yaml @@ -0,0 +1,6 @@ +objects: + DerivedKeyedSelfDesignatedClass: + frobnicator: Bob + length: 123 + KeyedSelfDesignatedClass: + frobnicator: Alice \ No newline at end of file From 5e71f3a05b1b1fbdbfdee830285feb31c9c64e33 Mon Sep 17 00:00:00 2001 From: Damien Goutte-Gattat Date: Fri, 26 Jun 2026 15:29:36 +0100 Subject: [PATCH 2/2] Commit re-generated code. --- ...ContainerOfKeyedSelfDesignatedObjects.java | 82 ++++++++++++++++++ .../base/DerivedKeyedSelfDesignatedClass.java | 67 +++++++++++++++ .../base/KeyedSelfDesignatedClass.java | 84 +++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 core/src/test/java/org/incenp/linkml/core/samples/base/ContainerOfKeyedSelfDesignatedObjects.java create mode 100644 core/src/test/java/org/incenp/linkml/core/samples/base/DerivedKeyedSelfDesignatedClass.java create mode 100644 core/src/test/java/org/incenp/linkml/core/samples/base/KeyedSelfDesignatedClass.java diff --git a/core/src/test/java/org/incenp/linkml/core/samples/base/ContainerOfKeyedSelfDesignatedObjects.java b/core/src/test/java/org/incenp/linkml/core/samples/base/ContainerOfKeyedSelfDesignatedObjects.java new file mode 100644 index 0000000..ed92db3 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/samples/base/ContainerOfKeyedSelfDesignatedObjects.java @@ -0,0 +1,82 @@ +package org.incenp.linkml.core.samples.base; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.incenp.linkml.core.annotations.Converter; +import org.incenp.linkml.core.annotations.ExtensionHolder; +import org.incenp.linkml.core.annotations.Identifier; +import org.incenp.linkml.core.annotations.Inlined; +import org.incenp.linkml.core.annotations.LinkURI; +import org.incenp.linkml.core.annotations.Required; +import org.incenp.linkml.core.annotations.SlotName; +import org.incenp.linkml.core.annotations.TypeDesignator; +import org.incenp.linkml.core.CurieConverter; + +@LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#ContainerOfKeyedSelfDesignatedObjects") +public class ContainerOfKeyedSelfDesignatedObjects { + + @Inlined + @LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#objects") + private List objects; + + public void setObjects(List objects) { + this.objects = objects; + } + + public List getObjects() { + return this.objects; + } + + public List getObjects(boolean set) { + if ( this.objects == null && set ) { + this.objects = new ArrayList<>(); + } + return this.objects; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Object o; + sb.append("ContainerOfKeyedSelfDesignatedObjects("); + if ( (o = this.getObjects()) != null ) { + sb.append("objects="); + sb.append(o); + sb.append(","); + } + sb.append(")"); + return sb.toString(); + } + + @Override + public boolean equals(final Object o) { + if ( o == this ) return true; + if ( !(o instanceof ContainerOfKeyedSelfDesignatedObjects) ) return false; + final ContainerOfKeyedSelfDesignatedObjects other = (ContainerOfKeyedSelfDesignatedObjects) o; + if ( !other.canEqual((Object) this)) return false; + final Object this$objects = this.getObjects(); + final Object other$objects = other.getObjects(); + if ( this$objects == null ? other$objects != null : !this$objects.equals(other$objects)) return false; + return true; + } + + protected boolean canEqual(final Object other) { + return other instanceof ContainerOfKeyedSelfDesignatedObjects; + } + + @Override + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $objects = this.getObjects(); + result = result * PRIME + ($objects == null ? 43 : $objects.hashCode()); + return result; + } +} \ No newline at end of file diff --git a/core/src/test/java/org/incenp/linkml/core/samples/base/DerivedKeyedSelfDesignatedClass.java b/core/src/test/java/org/incenp/linkml/core/samples/base/DerivedKeyedSelfDesignatedClass.java new file mode 100644 index 0000000..99ac744 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/samples/base/DerivedKeyedSelfDesignatedClass.java @@ -0,0 +1,67 @@ +package org.incenp.linkml.core.samples.base; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.incenp.linkml.core.annotations.Converter; +import org.incenp.linkml.core.annotations.ExtensionHolder; +import org.incenp.linkml.core.annotations.Identifier; +import org.incenp.linkml.core.annotations.Inlined; +import org.incenp.linkml.core.annotations.LinkURI; +import org.incenp.linkml.core.annotations.Required; +import org.incenp.linkml.core.annotations.SlotName; +import org.incenp.linkml.core.annotations.TypeDesignator; +import org.incenp.linkml.core.CurieConverter; + +@LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#DerivedKeyedSelfDesignatedClass") +public class DerivedKeyedSelfDesignatedClass extends KeyedSelfDesignatedClass { + + @LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#length") + private Integer length; + + public void setLength(Integer length) { + this.length = length; + } + + public Integer getLength() { + return this.length; + } + + @Override + public String toString() { + return "DerivedKeyedSelfDesignatedClass(type=" + this.getType() + ")"; + } + + @Override + public boolean equals(final Object o) { + if ( o == this ) return true; + if ( !(o instanceof DerivedKeyedSelfDesignatedClass) ) return false; + final DerivedKeyedSelfDesignatedClass other = (DerivedKeyedSelfDesignatedClass) o; + if ( !other.canEqual((Object) this)) return false; + if ( !super.equals(o) ) return false; + + final Object this$length = this.getLength(); + final Object other$length = other.getLength(); + if ( this$length == null ? other$length != null : !this$length.equals(other$length)) return false; + return true; + } + + protected boolean canEqual(final Object other) { + return other instanceof DerivedKeyedSelfDesignatedClass; + } + + @Override + public int hashCode() { + final int PRIME = 59; + int result = super.hashCode(); + final Object $length = this.getLength(); + result = result * PRIME + ($length == null ? 43 : $length.hashCode()); + return result; + } +} \ No newline at end of file diff --git a/core/src/test/java/org/incenp/linkml/core/samples/base/KeyedSelfDesignatedClass.java b/core/src/test/java/org/incenp/linkml/core/samples/base/KeyedSelfDesignatedClass.java new file mode 100644 index 0000000..cf595b2 --- /dev/null +++ b/core/src/test/java/org/incenp/linkml/core/samples/base/KeyedSelfDesignatedClass.java @@ -0,0 +1,84 @@ +package org.incenp.linkml.core.samples.base; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.incenp.linkml.core.annotations.Converter; +import org.incenp.linkml.core.annotations.ExtensionHolder; +import org.incenp.linkml.core.annotations.Identifier; +import org.incenp.linkml.core.annotations.Inlined; +import org.incenp.linkml.core.annotations.LinkURI; +import org.incenp.linkml.core.annotations.Required; +import org.incenp.linkml.core.annotations.SlotName; +import org.incenp.linkml.core.annotations.TypeDesignator; +import org.incenp.linkml.core.CurieConverter; + +@LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#KeyedSelfDesignatedClass") +public class KeyedSelfDesignatedClass { + + @Identifier(isGlobal = false) + @TypeDesignator + @Required + @LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#type") + private String type; + + @LinkURI("https://incenp.org/dvlpt/linkml-java/tests/samples#frobnicator") + private String frobnicator; + + public void setType(String type) { + this.type = type; + } + + public String getType() { + return this.type; + } + + public void setFrobnicator(String frobnicator) { + this.frobnicator = frobnicator; + } + + public String getFrobnicator() { + return this.frobnicator; + } + + @Override + public String toString() { + return "KeyedSelfDesignatedClass(type=" + this.getType() + ")"; + } + + @Override + public boolean equals(final Object o) { + if ( o == this ) return true; + if ( !(o instanceof KeyedSelfDesignatedClass) ) return false; + final KeyedSelfDesignatedClass other = (KeyedSelfDesignatedClass) o; + if ( !other.canEqual((Object) this)) return false; + final Object this$type = this.getType(); + final Object other$type = other.getType(); + if ( this$type == null ? other$type != null : !this$type.equals(other$type)) return false; + final Object this$frobnicator = this.getFrobnicator(); + final Object other$frobnicator = other.getFrobnicator(); + if ( this$frobnicator == null ? other$frobnicator != null : !this$frobnicator.equals(other$frobnicator)) return false; + return true; + } + + protected boolean canEqual(final Object other) { + return other instanceof KeyedSelfDesignatedClass; + } + + @Override + public int hashCode() { + final int PRIME = 59; + int result = 1; + final Object $type = this.getType(); + result = result * PRIME + ($type == null ? 43 : $type.hashCode()); + final Object $frobnicator = this.getFrobnicator(); + result = result * PRIME + ($frobnicator == null ? 43 : $frobnicator.hashCode()); + return result; + } +} \ No newline at end of file