diff --git a/base/src/main/java/com/evolveum/polygon/conndev/schema/AbstractAttributeBuilder.java b/base/src/main/java/com/evolveum/polygon/conndev/schema/AbstractAttributeBuilder.java index 8b21b94..c177406 100644 --- a/base/src/main/java/com/evolveum/polygon/conndev/schema/AbstractAttributeBuilder.java +++ b/base/src/main/java/com/evolveum/polygon/conndev/schema/AbstractAttributeBuilder.java @@ -24,9 +24,7 @@ import com.evolveum.polygon.conndev.spi.ValueMapping; import groovy.lang.Closure; import groovy.lang.DelegatesTo; -import org.identityconnectors.framework.common.objects.AttributeInfo; -import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; -import org.identityconnectors.framework.common.objects.EmbeddedObject; +import org.identityconnectors.framework.common.objects.*; import tools.jackson.databind.JsonNode; import java.util.HashMap; @@ -251,6 +249,22 @@ public ConnIdMapping connId() { return connIdBuilder; } + /** + * Special ConnId attributes (UID / NAME) are always exchanged as {@link String}, + * regardless of the protocol's native type. Returns the type this attribute's + * protocol mapping must produce, or {@code null} when no forcing applies. + */ + Class forcedConnIdType() { + // TOOD is this needed ? + if (connId().type().origin() == DefinitionValue.Origin.DECLARED) { + return null; + } + var connIdName = connId().name().value(); + return (Uid.NAME.equals(connIdName) || Name.NAME.equals(connIdName)) + ? String.class + : null; + } + /** * ConnId-side attribute metadata builder. * @@ -518,6 +532,7 @@ public Class suggestedConnIdType() { @Override public AttributeProtocolMapping build() { ValueMapping implementation; + var forced = forcedConnIdType(); if (this.implementation != null) { implementation = this.implementation; @@ -530,7 +545,10 @@ public AttributeProtocolMapping build() { } else { implementation = OpenApiValueMapping.from(type, openApiFormat); } - if (connIdType != null && !connIdType.equals(implementation.connIdType())) { + if(forced!=null && !forced.equals(implementation.connIdType())){ + + implementation = ValueTypeOverrideMapping.of(forced, implementation); + } else if (connIdType != null && !connIdType.equals(implementation.connIdType())) { // apply ConnId type override implementation = ValueTypeOverrideMapping.of(connIdType, implementation); } diff --git a/base/src/test/java/com/evolveum/polygon/conndev/schema/ForcedConnIdTypeTest.java b/base/src/test/java/com/evolveum/polygon/conndev/schema/ForcedConnIdTypeTest.java new file mode 100644 index 0000000..84495e0 --- /dev/null +++ b/base/src/test/java/com/evolveum/polygon/conndev/schema/ForcedConnIdTypeTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2026 Evolveum and contributors + * + * This work is licensed under European Union Public License v1.2. See LICENSE file for details. + * + */ +package com.evolveum.polygon.conndev.schema; + +import com.evolveum.polygon.conndev.api.ContextLookup; +import com.evolveum.polygon.conndev.concepts.DefinitionValue; +import com.evolveum.polygon.conndev.concepts.SourceLocation; +import com.evolveum.polygon.conndev.json.JsonAttributeMapping; +import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.Uid; +import org.testng.annotations.Test; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; + +import static org.testng.Assert.*; + +/** + * Verifies that special ConnId attributes (UID / NAME) are always exposed as + * {@link String} on the ConnId side, even when the protocol (JSON) native type + * not textual — e.g. a schema whose native {@code id} attribute has an integer + * wire type while being declared as the UID attribute. + *

+ * Covers both halves of the mechanism: + *

+ */ +public class ForcedConnIdTypeTest { + + private static TestObjectClass newObjectClass() { + var schemaBuilder = new BaseSchemaBuilder(StubConnector.class, ContextLookup.none()); + return new TestObjectClass(schemaBuilder, DefinitionValue.from("Test", SourceLocation.capture())); + } + + private static TestAttributeBuilder newUidAttribute() { + var attribute = newObjectClass().attribute("id"); + attribute.connId().name(Uid.NAME); + return attribute; + } + + @Test + public void uidAttribute_forcesString() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + + assertEquals(attribute.forcedConnIdType(), String.class); + } + + @Test + public void nameAttribute_forcesString() { + var attribute = newObjectClass().attribute("login"); + attribute.connId().name(Name.NAME); + attribute.json().type("integer"); + + assertEquals(attribute.forcedConnIdType(), String.class); + } + + @Test + public void regularAttribute_isNotForced() { + var attribute = newObjectClass().attribute("count"); + attribute.json().type("integer"); + + assertNull(attribute.forcedConnIdType()); + } + + @Test + public void defaultType_doesNotSuppressForcing() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + + assertEquals(attribute.forcedConnIdType(), String.class); + } + + @Test + public void detectedType_doesNotSuppressForcing() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + attribute.connId().type(DefinitionValue.detected( String.class)); + + assertEquals(attribute.forcedConnIdType(), String.class); + } + + @Test + public void uidAttribute_integerWire_mappingReportsStringConnIdType() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + + var mapping = attribute.build().json(); + + assertEquals(mapping.connIdType(), String.class); + } + + @Test + public void uidAttribute_integerWire_deserializesNumberAsString() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + + var mapping = attribute.build().json(); + Object value = mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42)); + + assertTrue(value instanceof String, "UID value should be deserialized as String, got: " + value); + assertEquals(value, "42"); + } + + @Test + public void uidAttribute_integerWire_serializesStringBackToNumber() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + + var mapping = attribute.build().json(); + ObjectNode parent = JsonNodeFactory.instance.objectNode(); + + mapping.toJsonNode(AttributeBuilder.build(Uid.NAME, "42"), parent); + + JsonNode node = parent.get("id"); + assertNotNull(node); + assertTrue(node.isIntegralNumber(), "Wire value should stay a JSON number, got: " + node); + assertEquals(node.intValue(), 42); + } + + @Test + public void nameAttribute_integerWire_forcedToString() { + var attribute = newObjectClass().attribute("login"); + attribute.connId().name(Name.NAME); + attribute.json().type("integer"); + + var mapping = attribute.build().json(); + + assertEquals(mapping.connIdType(), String.class); + assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(7)), "7"); + } + + @Test + public void uidAttribute_int64Wire_forcedToString() { + var attribute = newUidAttribute(); + attribute.json().type("integer").openApiFormat("int64"); + + var mapping = attribute.build().json(); + + assertEquals(mapping.connIdType(), String.class); + assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42)), "42"); + } + + @Test + public void uidAttribute_stringWire_noOverrideNeeded() { + var attribute = newUidAttribute(); + attribute.json().type("string"); + + var mapping = attribute.build().json(); + + assertEquals(mapping.connIdType(), String.class); + assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.stringNode("abc")), "abc"); + } + + @Test + public void regularAttribute_integerWire_keepsNativeType() { + var attribute = newObjectClass().attribute("count"); + attribute.json().type("integer"); + + var mapping = attribute.build().json(); + + assertEquals(mapping.connIdType(), Integer.class); + assertEquals(mapping.singleValueFromAttribute(JsonNodeFactory.instance.numberNode(42)), 42); + } + + @Test + public void uidAttribute_explicitType_mappingKeepsNativeType() { + var attribute = newUidAttribute(); + attribute.json().type("integer"); + attribute.connId().type(Integer.class); + + var definition = attribute.build(); + + assertEquals(definition.connId().getType(), Integer.class); + assertEquals(definition.json().connIdType(), Integer.class); + } +} \ No newline at end of file diff --git a/base/src/test/java/com/evolveum/polygon/conndev/schema/MappingRuleTest.java b/base/src/test/java/com/evolveum/polygon/conndev/schema/MappingRuleTest.java index 4de7e00..2a91cd0 100644 --- a/base/src/test/java/com/evolveum/polygon/conndev/schema/MappingRuleTest.java +++ b/base/src/test/java/com/evolveum/polygon/conndev/schema/MappingRuleTest.java @@ -32,21 +32,6 @@ */ public class MappingRuleTest { - private static final class StubConnector implements Connector { - @Override - public Configuration getConfiguration() { - return null; - } - - @Override - public void init(Configuration configuration) { - } - - @Override - public void dispose() { - } - } - /** Minimal attribute builder: B, A and R collapsed into a single self-referential type. */ private static final class TestAttributeBuilder extends BaseAttributeBuilder< TestAttributeBuilder, TestAttributeBuilder, TestAttributeBuilder, BaseAttributeDefinition> { diff --git a/base/src/test/java/com/evolveum/polygon/conndev/schema/StubConnector.java b/base/src/test/java/com/evolveum/polygon/conndev/schema/StubConnector.java new file mode 100644 index 0000000..ad78df0 --- /dev/null +++ b/base/src/test/java/com/evolveum/polygon/conndev/schema/StubConnector.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Evolveum and contributors + * + * This work is licensed under European Union Public License v1.2. See LICENSE file for details. + * + */ +package com.evolveum.polygon.conndev.schema; + +import org.identityconnectors.framework.spi.Configuration; +import org.identityconnectors.framework.spi.Connector; + +public class StubConnector implements Connector { + @Override + public Configuration getConfiguration() { + return null; + } + + @Override + public void init(Configuration configuration) { + } + + @Override + public void dispose() { + } +} \ No newline at end of file diff --git a/base/src/test/java/com/evolveum/polygon/conndev/schema/TestAttributeBuilder.java b/base/src/test/java/com/evolveum/polygon/conndev/schema/TestAttributeBuilder.java new file mode 100644 index 0000000..859f681 --- /dev/null +++ b/base/src/test/java/com/evolveum/polygon/conndev/schema/TestAttributeBuilder.java @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2026 Evolveum and contributors + * + * This work is licensed under European Union Public License v1.2. See LICENSE file for details. + * + */ +package com.evolveum.polygon.conndev.schema; + +import com.evolveum.polygon.conndev.concepts.DefinitionValue; + +/** Minimal attribute builder: B, A and R collapsed into a single self-referential type. */ +public final class TestAttributeBuilder extends BaseAttributeBuilder< + TestAttributeBuilder, TestAttributeBuilder, TestAttributeBuilder, BaseAttributeDefinition> { + TestAttributeBuilder(BaseObjectClassDefinitionBuilder parent, DefinitionValue name) { + super(parent, name); + } +} diff --git a/base/src/test/java/com/evolveum/polygon/conndev/schema/TestObjectClass.java b/base/src/test/java/com/evolveum/polygon/conndev/schema/TestObjectClass.java new file mode 100644 index 0000000..791182d --- /dev/null +++ b/base/src/test/java/com/evolveum/polygon/conndev/schema/TestObjectClass.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2026 Evolveum and contributors + * + * This work is licensed under European Union Public License v1.2. See LICENSE file for details. + * + */ +package com.evolveum.polygon.conndev.schema; + +import com.evolveum.polygon.conndev.concepts.DefinitionValue; + +/** Minimal object class builder pairing with {@link TestAttributeBuilder}. */ +public final class TestObjectClass extends BaseObjectClassDefinitionBuilder< + TestObjectClass, + BaseObjectClassDefinition, + TestAttributeBuilder, + TestAttributeBuilder, + TestAttributeBuilder, + BaseAttributeDefinition> { + + TestObjectClass(BaseSchemaBuilder parent, DefinitionValue name) { + super(parent, name); + } + + @Override + protected TestAttributeBuilder newAttribute(DefinitionValue def) { + return new TestAttributeBuilder(this, def); + } +} \ No newline at end of file